@camstack/addon-smtp-nodemailer 1.2.150 → 1.2.151

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.
@@ -6018,7 +6018,7 @@ var ZodIssueCode = {
6018
6018
  var ZodFirstPartyTypeKind;
6019
6019
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6020
6020
  //#endregion
6021
- //#region ../types/dist/sleep-CmjWurK5.mjs
6021
+ //#region ../types/dist/sleep-D7ZKm4nb.mjs
6022
6022
  /**
6023
6023
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6024
6024
  * window to float samples (D455).
@@ -6727,10 +6727,33 @@ function integrationRowsApiOver(api) {
6727
6727
  };
6728
6728
  }
6729
6729
  var PREFIX = "records/";
6730
+ /**
6731
+ * Cross-realm brand. An addon bundles its own copy of this module (D15) while
6732
+ * `ctx.records` is the HOST's door, so an error thrown by the host is an
6733
+ * instance of the host's classes, not of the addon's. The brand plus the code
6734
+ * make `instanceof` answer by identity of the failure, not of the copy.
6735
+ */
6736
+ var RECORDS_ERROR_BRAND = Symbol.for("camstack.records-error");
6737
+ function carriesBrand(value) {
6738
+ return typeof value === "object" && value !== null && Reflect.get(value, RECORDS_ERROR_BRAND) === true;
6739
+ }
6730
6740
  /** The base of every records failure. `detail` is the human part of the message. */
6731
6741
  var RecordsError = class extends Error {
6742
+ /** The code a subclass stands for; the base stands for every code. */
6743
+ static code = void 0;
6744
+ /**
6745
+ * `instanceof` across copies of this module: any branded error is a
6746
+ * `RecordsError`, and is an instance of a subclass only when its code is the
6747
+ * subclass's code. Same-copy answers are unchanged.
6748
+ */
6749
+ static [Symbol.hasInstance](value) {
6750
+ if (Function.prototype[Symbol.hasInstance].call(this, value)) return true;
6751
+ if (!carriesBrand(value)) return false;
6752
+ return this.code === void 0 || value.code === this.code;
6753
+ }
6732
6754
  code;
6733
6755
  detail;
6756
+ [RECORDS_ERROR_BRAND] = true;
6734
6757
  constructor(code, detail) {
6735
6758
  super(`${PREFIX}${code}: ${detail}`);
6736
6759
  this.name = "RecordsError";
@@ -6739,29 +6762,33 @@ var RecordsError = class extends Error {
6739
6762
  }
6740
6763
  };
6741
6764
  /** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
6742
- var RevConflictError = class extends RecordsError {
6765
+ var RevConflictError = class RevConflictError extends RecordsError {
6766
+ static code = "rev-conflict";
6743
6767
  constructor(detail) {
6744
- super("rev-conflict", detail);
6768
+ super(RevConflictError.code, detail);
6745
6769
  this.name = "RevConflictError";
6746
6770
  }
6747
6771
  };
6748
6772
  /** The keychain could not store or serve a value: explicit, never an empty string. */
6749
- var RecordSecretUnavailableError = class extends RecordsError {
6773
+ var RecordSecretUnavailableError = class RecordSecretUnavailableError extends RecordsError {
6774
+ static code = "secret-unavailable";
6750
6775
  constructor(detail) {
6751
- super("secret-unavailable", detail);
6776
+ super(RecordSecretUnavailableError.code, detail);
6752
6777
  this.name = "RecordSecretUnavailableError";
6753
6778
  }
6754
6779
  };
6755
- var InvalidRecordError = class extends RecordsError {
6780
+ var InvalidRecordError = class InvalidRecordError extends RecordsError {
6781
+ static code = "invalid-record";
6756
6782
  constructor(detail) {
6757
- super("invalid-record", detail);
6783
+ super(InvalidRecordError.code, detail);
6758
6784
  this.name = "InvalidRecordError";
6759
6785
  }
6760
6786
  };
6761
6787
  /** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
6762
- var DeclarationRefusedError = class extends RecordsError {
6788
+ var DeclarationRefusedError = class DeclarationRefusedError extends RecordsError {
6789
+ static code = "declaration-refused";
6763
6790
  constructor(detail) {
6764
- super("declaration-refused", detail);
6791
+ super(DeclarationRefusedError.code, detail);
6765
6792
  this.name = "DeclarationRefusedError";
6766
6793
  }
6767
6794
  };
@@ -9556,286 +9583,6 @@ function normalizeAddonInitResult(result) {
9556
9583
  if (Array.isArray(result)) return { providers: result };
9557
9584
  return result;
9558
9585
  }
9559
- /**
9560
- * The share-view scope kinds a share-link token can carry. The hub mints
9561
- * and validates them (`ShareViewGrantSchema` builds its
9562
- * enum from this list); a data plane names the kinds it ACCEPTS in `share`.
9563
- */
9564
- var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
9565
- /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
9566
- var PeerBytesTicketSchema = object({
9567
- /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
9568
- url: string().min(1),
9569
- /**
9570
- * The HOST node this URL means something on — the hub or a named agent,
9571
- * never a runner. {@link AddonPeerBytes.open} compares it to its own and
9572
- * refuses `cross-node` by name when they differ, without dialling.
9573
- */
9574
- hostNodeId: string().min(1),
9575
- expiresAtMs: number().int().nonnegative(),
9576
- /**
9577
- * What the producer DECLARED the body to be, when it knows — `null` when it
9578
- * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
9579
- * must be able to tell "the producer did not say" from "the body is empty".
9580
- */
9581
- declaredBytes: number().int().nonnegative().nullable()
9582
- });
9583
- function defOf(schema) {
9584
- const zodBag = Reflect.get(schema, "_zod");
9585
- if (typeof zodBag !== "object" || zodBag === null) return {};
9586
- const def = Reflect.get(zodBag, "def");
9587
- if (typeof def !== "object" || def === null) return {};
9588
- return def;
9589
- }
9590
- function declaredHere(schema) {
9591
- return globalRegistry.get(schema)?.secret === true;
9592
- }
9593
- /** Wrapper kinds whose `innerType` is the field itself. */
9594
- var WRAPPERS = new Set([
9595
- "optional",
9596
- "nullable",
9597
- "default",
9598
- "prefault",
9599
- "catch",
9600
- "readonly",
9601
- "nonoptional"
9602
- ]);
9603
- /**
9604
- * The schema layers that make up ONE field: the schema itself and every
9605
- * wrapper below it (optional / default / nullable / readonly / catch), plus
9606
- * both sides of a pipe (`z.preprocess` puts the real schema on `out`,
9607
- * `.transform` on `in`).
9608
- */
9609
- function layersOf(schema) {
9610
- const out = [];
9611
- const queue = [schema];
9612
- while (queue.length > 0) {
9613
- const s = queue.shift();
9614
- if (!s || out.includes(s)) continue;
9615
- out.push(s);
9616
- const def = defOf(s);
9617
- if (def.type !== void 0 && WRAPPERS.has(def.type) && def.innerType) queue.push(def.innerType);
9618
- if (def.type === "pipe") {
9619
- if (def.in) queue.push(def.in);
9620
- if (def.out) queue.push(def.out);
9621
- }
9622
- }
9623
- return out;
9624
- }
9625
- /** True when any layer of this field declares `.meta({ secret: true })`. */
9626
- function isZodSecret(schema) {
9627
- return layersOf(schema).some(declaredHere);
9628
- }
9629
- /**
9630
- * Every declared secret path in a config schema: dotted keys, `[]` for "each
9631
- * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
9632
- * map" (`applianceSecrets{}`). A declared
9633
- * object or array is reported as one path and not descended into — everything
9634
- * under it is secret.
9635
- */
9636
- function collectZodSecretPaths(schema, prefix = "") {
9637
- if (isZodSecret(schema)) return prefix.length > 0 ? [prefix] : [];
9638
- const paths = [];
9639
- for (const layer of layersOf(schema)) {
9640
- const def = defOf(layer);
9641
- if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
9642
- else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
9643
- else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
9644
- else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
9645
- else if (def.type === "intersection") {
9646
- if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
9647
- if (def.right) paths.push(...collectZodSecretPaths(def.right, prefix));
9648
- }
9649
- }
9650
- return [...new Set(paths)];
9651
- }
9652
- /**
9653
- * Record kinds (D881): the declaration an addon makes of one kind of fact, the
9654
- * Zod row schema that describes it, and the helper that turns the two into the
9655
- * JSON the engine stores.
9656
- *
9657
- * A kind is declared ONCE, next to the code that owns it, and the engine's
9658
- * reconciler creates its indexes - nothing creates DDL at runtime from a write.
9659
- * Everything the generic machinery needs (secret fields, the entry descriptor)
9660
- * is DERIVED from the Zod schema: `.meta({ secret: true })` marks a credential,
9661
- * never a field name (D879).
9662
- */
9663
- /** A JSON field path: `trackId`, `auth.accessToken`. Charset-restricted: it is rendered into DDL. */
9664
- var RecordFieldPathSchema = string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,63}(\.[A-Za-z_][A-Za-z0-9_]{0,63}){0,3}$/);
9665
- var RecordIndexSchema = object({
9666
- name: string().regex(/^[a-z][a-z0-9_]{0,31}$/),
9667
- fields: array(RecordFieldPathSchema).min(1).max(4),
9668
- unique: boolean().optional()
9669
- }).strict();
9670
- var RecordRetentionSchema = union([
9671
- object({
9672
- ttlMs: number().int().positive(),
9673
- from: _enum(["at", "write"])
9674
- }).strict(),
9675
- object({
9676
- maxRows: number().int().positive().optional(),
9677
- maxRowsPerDevice: number().int().positive().optional()
9678
- }).strict().refine((r) => r.maxRows !== void 0 || r.maxRowsPerDevice !== void 0, { message: "name maxRows, maxRowsPerDevice or both" }),
9679
- object({
9680
- owner: string().min(1),
9681
- reason: string().min(1)
9682
- }).strict(),
9683
- object({ none: string().min(1) }).strict()
9684
- ]);
9685
- /** The descriptor a generic entries UI reads (D881 "generic entries"). */
9686
- var EntryDescriptorSchema = object({
9687
- title: string().min(1),
9688
- /** The record body field shown as the entry's name. */
9689
- nameField: string().min(1),
9690
- /** JSON Schema of the entry body; `secret: true` marks a password field. */
9691
- fields: record(string(), unknown())
9692
- }).strict();
9693
- /** What crosses the wire from an addon: JSON only, no Zod. An unknown key is refused. */
9694
- var RecordKindDeclarationSchema = object({
9695
- database: _enum(RECORD_DATABASES).default("settings"),
9696
- class: _enum(RECORD_CLASSES),
9697
- writeTrigger: string().min(1),
9698
- device: _enum([
9699
- "required",
9700
- "optional",
9701
- "none"
9702
- ]).default("none"),
9703
- at: _enum(["required", "none"]).default("none"),
9704
- indexes: array(RecordIndexSchema).max(8).default([]),
9705
- retention: RecordRetentionSchema,
9706
- deviceRemoval: _enum(["delete", "none"]).default("none"),
9707
- maxDataBytes: number().int().positive().max(RECORD_MAX_DATA_BYTES).default(RECORD_DEFAULT_MAX_DATA_BYTES),
9708
- /** Derived from the Zod schema; never written by hand. */
9709
- secretPaths: array(string().min(1)).default([]),
9710
- /** The id key of each array that carries a secret (`{ headers: 'name' }`). */
9711
- secretArrayKeys: record(string(), string()).default({}),
9712
- /** Kinds that must agree through `transact` sit in one file under one family name. */
9713
- transactFamily: string().min(1).optional(),
9714
- entry: EntryDescriptorSchema.optional()
9715
- }).strict();
9716
- /** The kind names: lower-case, dash separated, short enough for an index name. */
9717
- var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
9718
- /** Cross-field rules a declaration must satisfy. Throws {@link DeclarationRefusedError}. */
9719
- function assertValidDeclaration(name, decl) {
9720
- const refuse = (why) => {
9721
- throw new DeclarationRefusedError(`kind "${name}": ${why}`);
9722
- };
9723
- if (!RecordKindNameSchema.safeParse(name).success) refuse("the name must be lower-case-dashed");
9724
- const seen = /* @__PURE__ */ new Set();
9725
- for (const index of decl.indexes) {
9726
- if (seen.has(index.name)) refuse(`index name "${index.name}" is declared twice`);
9727
- seen.add(index.name);
9728
- }
9729
- const { retention } = decl;
9730
- const hasSecrets = decl.secretPaths.length > 0;
9731
- if ("ttlMs" in retention) {
9732
- if (retention.from === "at" && decl.at !== "required") refuse("a ttl from \"at\" needs at: \"required\"");
9733
- if (hasSecrets) refuse("a kind with secret fields cannot use engine TTL (its keychain values would be orphaned)");
9734
- }
9735
- if ("maxRows" in retention || "maxRowsPerDevice" in retention) {
9736
- if (retention.maxRowsPerDevice !== void 0 && decl.device !== "required") refuse("maxRowsPerDevice needs device: \"required\"");
9737
- if (decl.at !== "required") refuse("a row bound trims the oldest \"at\": it needs at: \"required\"");
9738
- if (hasSecrets) refuse("a kind with secret fields cannot use an engine row bound");
9739
- const bound = Math.max(retention.maxRows ?? 0, retention.maxRowsPerDevice ?? 0);
9740
- 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)`);
9741
- }
9742
- if (decl.deviceRemoval === "delete" && decl.device === "none") refuse("deviceRemoval: \"delete\" needs a device: \"required\" or \"optional\"");
9743
- 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`);
9744
- }
9745
- function bodyFieldsOf(schema) {
9746
- return { ...toJSONSchema(schema, {
9747
- unrepresentable: "any",
9748
- io: "input"
9749
- }) };
9750
- }
9751
- function isRecordLike(value) {
9752
- return typeof value === "object" && value !== null && !Array.isArray(value);
9753
- }
9754
- /** Does every branch of the body schema carry `field`? (A discriminated union has several.) */
9755
- function hasNameField(json, field) {
9756
- const branches = json["oneOf"] ?? json["anyOf"];
9757
- if (Array.isArray(branches)) return branches.length > 0 && branches.every((b) => isRecordLike(b) && hasNameField(b, field));
9758
- const properties = json["properties"];
9759
- return isRecordLike(properties) && Object.prototype.hasOwnProperty.call(properties, field);
9760
- }
9761
- /**
9762
- * Declare the kinds of a package. The keys are the kind names. The result is
9763
- * what `ctx.records.declare` sends to the engine and what `ctx.records.kind`
9764
- * types its handle from.
9765
- */
9766
- function defineRecordKinds(specs) {
9767
- const out = {};
9768
- for (const [name, spec] of Object.entries(specs)) {
9769
- const secretPaths = collectZodSecretPaths(spec.schema);
9770
- const nameField = spec.entry?.nameField ?? "name";
9771
- const fields = bodyFieldsOf(spec.schema);
9772
- if (spec.entry !== void 0 && !hasNameField(fields, nameField)) throw new DeclarationRefusedError(`kind "${name}": an entry kind's schema needs a "${nameField}" field`);
9773
- const parsed = RecordKindDeclarationSchema.parse({
9774
- database: spec.database,
9775
- class: spec.class,
9776
- writeTrigger: spec.writeTrigger,
9777
- device: spec.device,
9778
- at: spec.at,
9779
- indexes: spec.indexes === void 0 ? void 0 : [...spec.indexes],
9780
- retention: spec.retention,
9781
- deviceRemoval: spec.deviceRemoval,
9782
- maxDataBytes: spec.maxDataBytes,
9783
- secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
9784
- secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
9785
- transactFamily: spec.transactFamily,
9786
- entry: spec.entry === void 0 ? void 0 : {
9787
- title: spec.entry.title,
9788
- nameField,
9789
- fields
9790
- }
9791
- });
9792
- assertValidDeclaration(name, parsed);
9793
- out[name] = {
9794
- name,
9795
- schema: spec.schema,
9796
- declaration: parsed
9797
- };
9798
- }
9799
- return out;
9800
- }
9801
- /**
9802
- * Entry kinds (D881): every list of user-created entries (notification
9803
- * targets, brokers, LLM profiles) is ONE record kind with an id, a name and a
9804
- * kind-specific body, and the admin UI lists/adds/edits/deletes all of them
9805
- * through the `record-entries` capability with password fields derived from
9806
- * the schema (`.meta({ secret: true })`).
9807
- */
9808
- /** The body of a plain entry: a name and the fields the kind adds. */
9809
- function entryBody(shape) {
9810
- return object({
9811
- name: string().min(1),
9812
- enabled: boolean().optional(),
9813
- ...shape
9814
- });
9815
- }
9816
- /**
9817
- * Declare one entry kind. An entry is configuration: never trimmed by the
9818
- * engine, never aged, belonging to no device.
9819
- */
9820
- function defineEntryKind(name, spec) {
9821
- const defined = defineRecordKinds({ [name]: {
9822
- schema: spec.schema,
9823
- class: "config",
9824
- writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
9825
- retention: { none: "an entry goes only when the operator deletes it" },
9826
- ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
9827
- ...spec.database === void 0 ? {} : { database: spec.database },
9828
- ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9829
- ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
9830
- ...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
9831
- entry: {
9832
- title: spec.title,
9833
- ...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
9834
- }
9835
- } })[name];
9836
- if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
9837
- return defined;
9838
- }
9839
9586
  object({
9840
9587
  field: string().min(1),
9841
9588
  on: literal("advance")
@@ -9924,6 +9671,286 @@ function optionalMethod(input, output, options) {
9924
9671
  providerOptional: true
9925
9672
  };
9926
9673
  }
9674
+ function defOf(schema) {
9675
+ const zodBag = Reflect.get(schema, "_zod");
9676
+ if (typeof zodBag !== "object" || zodBag === null) return {};
9677
+ const def = Reflect.get(zodBag, "def");
9678
+ if (typeof def !== "object" || def === null) return {};
9679
+ return def;
9680
+ }
9681
+ function declaredHere(schema) {
9682
+ return globalRegistry.get(schema)?.secret === true;
9683
+ }
9684
+ /** Wrapper kinds whose `innerType` is the field itself. */
9685
+ var WRAPPERS = new Set([
9686
+ "optional",
9687
+ "nullable",
9688
+ "default",
9689
+ "prefault",
9690
+ "catch",
9691
+ "readonly",
9692
+ "nonoptional"
9693
+ ]);
9694
+ /**
9695
+ * The schema layers that make up ONE field: the schema itself and every
9696
+ * wrapper below it (optional / default / nullable / readonly / catch), plus
9697
+ * both sides of a pipe (`z.preprocess` puts the real schema on `out`,
9698
+ * `.transform` on `in`).
9699
+ */
9700
+ function layersOf(schema) {
9701
+ const out = [];
9702
+ const queue = [schema];
9703
+ while (queue.length > 0) {
9704
+ const s = queue.shift();
9705
+ if (!s || out.includes(s)) continue;
9706
+ out.push(s);
9707
+ const def = defOf(s);
9708
+ if (def.type !== void 0 && WRAPPERS.has(def.type) && def.innerType) queue.push(def.innerType);
9709
+ if (def.type === "pipe") {
9710
+ if (def.in) queue.push(def.in);
9711
+ if (def.out) queue.push(def.out);
9712
+ }
9713
+ }
9714
+ return out;
9715
+ }
9716
+ /** True when any layer of this field declares `.meta({ secret: true })`. */
9717
+ function isZodSecret(schema) {
9718
+ return layersOf(schema).some(declaredHere);
9719
+ }
9720
+ /**
9721
+ * Every declared secret path in a config schema: dotted keys, `[]` for "each
9722
+ * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
9723
+ * map" (`applianceSecrets{}`). A declared
9724
+ * object or array is reported as one path and not descended into — everything
9725
+ * under it is secret.
9726
+ */
9727
+ function collectZodSecretPaths(schema, prefix = "") {
9728
+ if (isZodSecret(schema)) return prefix.length > 0 ? [prefix] : [];
9729
+ const paths = [];
9730
+ for (const layer of layersOf(schema)) {
9731
+ const def = defOf(layer);
9732
+ if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
9733
+ else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
9734
+ else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
9735
+ else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
9736
+ else if (def.type === "intersection") {
9737
+ if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
9738
+ if (def.right) paths.push(...collectZodSecretPaths(def.right, prefix));
9739
+ }
9740
+ }
9741
+ return [...new Set(paths)];
9742
+ }
9743
+ /**
9744
+ * Record kinds (D881): the declaration an addon makes of one kind of fact, the
9745
+ * Zod row schema that describes it, and the helper that turns the two into the
9746
+ * JSON the engine stores.
9747
+ *
9748
+ * A kind is declared ONCE, next to the code that owns it, and the engine's
9749
+ * reconciler creates its indexes - nothing creates DDL at runtime from a write.
9750
+ * Everything the generic machinery needs (secret fields, the entry descriptor)
9751
+ * is DERIVED from the Zod schema: `.meta({ secret: true })` marks a credential,
9752
+ * never a field name (D879).
9753
+ */
9754
+ /** A JSON field path: `trackId`, `auth.accessToken`. Charset-restricted: it is rendered into DDL. */
9755
+ var RecordFieldPathSchema = string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,63}(\.[A-Za-z_][A-Za-z0-9_]{0,63}){0,3}$/);
9756
+ var RecordIndexSchema = object({
9757
+ name: string().regex(/^[a-z][a-z0-9_]{0,31}$/),
9758
+ fields: array(RecordFieldPathSchema).min(1).max(4),
9759
+ unique: boolean().optional()
9760
+ }).strict();
9761
+ var RecordRetentionSchema = union([
9762
+ object({
9763
+ ttlMs: number().int().positive(),
9764
+ from: _enum(["at", "write"])
9765
+ }).strict(),
9766
+ object({
9767
+ maxRows: number().int().positive().optional(),
9768
+ maxRowsPerDevice: number().int().positive().optional()
9769
+ }).strict().refine((r) => r.maxRows !== void 0 || r.maxRowsPerDevice !== void 0, { message: "name maxRows, maxRowsPerDevice or both" }),
9770
+ object({
9771
+ owner: string().min(1),
9772
+ reason: string().min(1)
9773
+ }).strict(),
9774
+ object({ none: string().min(1) }).strict()
9775
+ ]);
9776
+ /** The descriptor a generic entries UI reads (D881 "generic entries"). */
9777
+ var EntryDescriptorSchema = object({
9778
+ title: string().min(1),
9779
+ /** The record body field shown as the entry's name. */
9780
+ nameField: string().min(1),
9781
+ /** JSON Schema of the entry body; `secret: true` marks a password field. */
9782
+ fields: record(string(), unknown())
9783
+ }).strict();
9784
+ /** What crosses the wire from an addon: JSON only, no Zod. An unknown key is refused. */
9785
+ var RecordKindDeclarationSchema = object({
9786
+ database: _enum(RECORD_DATABASES).default("settings"),
9787
+ class: _enum(RECORD_CLASSES),
9788
+ writeTrigger: string().min(1),
9789
+ device: _enum([
9790
+ "required",
9791
+ "optional",
9792
+ "none"
9793
+ ]).default("none"),
9794
+ at: _enum(["required", "none"]).default("none"),
9795
+ indexes: array(RecordIndexSchema).max(8).default([]),
9796
+ retention: RecordRetentionSchema,
9797
+ deviceRemoval: _enum(["delete", "none"]).default("none"),
9798
+ maxDataBytes: number().int().positive().max(RECORD_MAX_DATA_BYTES).default(RECORD_DEFAULT_MAX_DATA_BYTES),
9799
+ /** Derived from the Zod schema; never written by hand. */
9800
+ secretPaths: array(string().min(1)).default([]),
9801
+ /** The id key of each array that carries a secret (`{ headers: 'name' }`). */
9802
+ secretArrayKeys: record(string(), string()).default({}),
9803
+ /** Kinds that must agree through `transact` sit in one file under one family name. */
9804
+ transactFamily: string().min(1).optional(),
9805
+ entry: EntryDescriptorSchema.optional()
9806
+ }).strict();
9807
+ /** The kind names: lower-case, dash separated, short enough for an index name. */
9808
+ var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
9809
+ /** Cross-field rules a declaration must satisfy. Throws {@link DeclarationRefusedError}. */
9810
+ function assertValidDeclaration(name, decl) {
9811
+ const refuse = (why) => {
9812
+ throw new DeclarationRefusedError(`kind "${name}": ${why}`);
9813
+ };
9814
+ if (!RecordKindNameSchema.safeParse(name).success) refuse("the name must be lower-case-dashed");
9815
+ const seen = /* @__PURE__ */ new Set();
9816
+ for (const index of decl.indexes) {
9817
+ if (seen.has(index.name)) refuse(`index name "${index.name}" is declared twice`);
9818
+ seen.add(index.name);
9819
+ }
9820
+ const { retention } = decl;
9821
+ const hasSecrets = decl.secretPaths.length > 0;
9822
+ if ("ttlMs" in retention) {
9823
+ if (retention.from === "at" && decl.at !== "required") refuse("a ttl from \"at\" needs at: \"required\"");
9824
+ if (hasSecrets) refuse("a kind with secret fields cannot use engine TTL (its keychain values would be orphaned)");
9825
+ }
9826
+ if ("maxRows" in retention || "maxRowsPerDevice" in retention) {
9827
+ if (retention.maxRowsPerDevice !== void 0 && decl.device !== "required") refuse("maxRowsPerDevice needs device: \"required\"");
9828
+ if (decl.at !== "required") refuse("a row bound trims the oldest \"at\": it needs at: \"required\"");
9829
+ if (hasSecrets) refuse("a kind with secret fields cannot use an engine row bound");
9830
+ const bound = Math.max(retention.maxRows ?? 0, retention.maxRowsPerDevice ?? 0);
9831
+ 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)`);
9832
+ }
9833
+ if (decl.deviceRemoval === "delete" && decl.device === "none") refuse("deviceRemoval: \"delete\" needs a device: \"required\" or \"optional\"");
9834
+ 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`);
9835
+ }
9836
+ function bodyFieldsOf(schema) {
9837
+ return { ...toJSONSchema(schema, {
9838
+ unrepresentable: "any",
9839
+ io: "input"
9840
+ }) };
9841
+ }
9842
+ function isRecordLike(value) {
9843
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9844
+ }
9845
+ /** Does every branch of the body schema carry `field`? (A discriminated union has several.) */
9846
+ function hasNameField(json, field) {
9847
+ const branches = json["oneOf"] ?? json["anyOf"];
9848
+ if (Array.isArray(branches)) return branches.length > 0 && branches.every((b) => isRecordLike(b) && hasNameField(b, field));
9849
+ const properties = json["properties"];
9850
+ return isRecordLike(properties) && Object.prototype.hasOwnProperty.call(properties, field);
9851
+ }
9852
+ /**
9853
+ * Declare the kinds of a package. The keys are the kind names. The result is
9854
+ * what `ctx.records.declare` sends to the engine and what `ctx.records.kind`
9855
+ * types its handle from.
9856
+ */
9857
+ function defineRecordKinds(specs) {
9858
+ const out = {};
9859
+ for (const [name, spec] of Object.entries(specs)) {
9860
+ const secretPaths = collectZodSecretPaths(spec.schema);
9861
+ const nameField = spec.entry?.nameField ?? "name";
9862
+ const fields = bodyFieldsOf(spec.schema);
9863
+ if (spec.entry !== void 0 && !hasNameField(fields, nameField)) throw new DeclarationRefusedError(`kind "${name}": an entry kind's schema needs a "${nameField}" field`);
9864
+ const parsed = RecordKindDeclarationSchema.parse({
9865
+ database: spec.database,
9866
+ class: spec.class,
9867
+ writeTrigger: spec.writeTrigger,
9868
+ device: spec.device,
9869
+ at: spec.at,
9870
+ indexes: spec.indexes === void 0 ? void 0 : [...spec.indexes],
9871
+ retention: spec.retention,
9872
+ deviceRemoval: spec.deviceRemoval,
9873
+ maxDataBytes: spec.maxDataBytes,
9874
+ secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
9875
+ secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
9876
+ transactFamily: spec.transactFamily,
9877
+ entry: spec.entry === void 0 ? void 0 : {
9878
+ title: spec.entry.title,
9879
+ nameField,
9880
+ fields
9881
+ }
9882
+ });
9883
+ assertValidDeclaration(name, parsed);
9884
+ out[name] = {
9885
+ name,
9886
+ schema: spec.schema,
9887
+ declaration: parsed
9888
+ };
9889
+ }
9890
+ return out;
9891
+ }
9892
+ /**
9893
+ * Entry kinds (D881): every list of user-created entries (notification
9894
+ * targets, brokers, LLM profiles) is ONE record kind with an id, a name and a
9895
+ * kind-specific body, and the admin UI lists/adds/edits/deletes all of them
9896
+ * through the `record-entries` capability with password fields derived from
9897
+ * the schema (`.meta({ secret: true })`).
9898
+ */
9899
+ /** The body of a plain entry: a name and the fields the kind adds. */
9900
+ function entryBody(shape) {
9901
+ return object({
9902
+ name: string().min(1),
9903
+ enabled: boolean().optional(),
9904
+ ...shape
9905
+ });
9906
+ }
9907
+ /**
9908
+ * Declare one entry kind. An entry is configuration: never trimmed by the
9909
+ * engine, never aged, belonging to no device.
9910
+ */
9911
+ function defineEntryKind(name, spec) {
9912
+ const defined = defineRecordKinds({ [name]: {
9913
+ schema: spec.schema,
9914
+ class: "config",
9915
+ writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
9916
+ retention: { none: "an entry goes only when the operator deletes it" },
9917
+ ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
9918
+ ...spec.database === void 0 ? {} : { database: spec.database },
9919
+ ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9920
+ ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
9921
+ ...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
9922
+ entry: {
9923
+ title: spec.title,
9924
+ ...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
9925
+ }
9926
+ } })[name];
9927
+ if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
9928
+ return defined;
9929
+ }
9930
+ /**
9931
+ * The share-view scope kinds a share-link token can carry. The hub mints
9932
+ * and validates them (`ShareViewGrantSchema` builds its
9933
+ * enum from this list); a data plane names the kinds it ACCEPTS in `share`.
9934
+ */
9935
+ var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
9936
+ /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
9937
+ var PeerBytesTicketSchema = object({
9938
+ /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
9939
+ url: string().min(1),
9940
+ /**
9941
+ * The HOST node this URL means something on — the hub or a named agent,
9942
+ * never a runner. {@link AddonPeerBytes.open} compares it to its own and
9943
+ * refuses `cross-node` by name when they differ, without dialling.
9944
+ */
9945
+ hostNodeId: string().min(1),
9946
+ expiresAtMs: number().int().nonnegative(),
9947
+ /**
9948
+ * What the producer DECLARED the body to be, when it knows — `null` when it
9949
+ * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
9950
+ * must be able to tell "the producer did not say" from "the body is empty".
9951
+ */
9952
+ declaredBytes: number().int().nonnegative().nullable()
9953
+ });
9927
9954
  var READING_QUANTITIES = [
9928
9955
  "temperature",
9929
9956
  "pressure",
@@ -10502,7 +10529,6 @@ var CAP_NAMES = Object.freeze([
10502
10529
  "climate-control",
10503
10530
  "color",
10504
10531
  "composer",
10505
- "connection-test",
10506
10532
  "connectivity",
10507
10533
  "consumables",
10508
10534
  "contact",
@@ -12522,8 +12548,7 @@ Object.freeze({
12522
12548
  "offerArtifact": 3e5,
12523
12549
  "readResult": 3e5,
12524
12550
  "submitJob": 18e5,
12525
- "uploadDataset": 72e5,
12526
- "validateCredentials": 3e4
12551
+ "uploadDataset": 72e5
12527
12552
  }
12528
12553
  });
12529
12554
  object({
@@ -13938,6 +13963,17 @@ function composeInstanceKinds(parts, base) {
13938
13963
  if (part === void 0) throw new UnknownInstanceKindError(kindId);
13939
13964
  return part;
13940
13965
  };
13966
+ /**
13967
+ * The part a DRAFT test runs on. A named kind is that kind; an unnamed one is the provider's only
13968
+ * testable kind. Several testable kinds and none named is a typed refusal that lists them, never a throw.
13969
+ */
13970
+ const draftPart = (kindId) => {
13971
+ if (kindId !== void 0) return parts.find((candidate) => candidate.kind.id === kindId) ?? { error: `Unknown instance kind '${kindId}'` };
13972
+ const testable = parts.filter((part) => part.kind.canTest !== false);
13973
+ const only = testable.length === 1 ? testable[0] : parts.length === 1 ? parts[0] : void 0;
13974
+ if (only !== void 0) return only;
13975
+ return { error: `This provider has several kinds: name one of ${(testable.length > 0 ? testable : parts).map((part) => part.kind.id).join(", ")}` };
13976
+ };
13941
13977
  const holderOf = async (id) => {
13942
13978
  for (const part of parts) if (await part.provider.getInstance({ id }) !== null) return part;
13943
13979
  throw new UnknownInstanceError(id);
@@ -13973,7 +14009,13 @@ function composeInstanceKinds(parts, base) {
13973
14009
  setInstanceEnabled: async (input) => (await holderOf(input.id)).provider.setInstanceEnabled(input),
13974
14010
  deleteInstance: async (input) => (await holderOf(input.id)).provider.deleteInstance(input),
13975
14011
  testInstance: async (input) => {
13976
- const test = (input.id === null ? byKind(input.kind) : await holderOf(input.id)).provider.testInstance;
14012
+ const resolved = input.id === null ? draftPart(input.kind) : await holderOf(input.id);
14013
+ if ("error" in resolved) return {
14014
+ success: false,
14015
+ status: "inconclusive",
14016
+ error: resolved.error
14017
+ };
14018
+ const test = resolved.provider.testInstance;
13977
14019
  if (test === void 0) return {
13978
14020
  success: false,
13979
14021
  status: "inconclusive",
@@ -13983,6 +14025,432 @@ function composeInstanceKinds(parts, base) {
13983
14025
  }
13984
14026
  };
13985
14027
  }
14028
+ /**
14029
+ * notification-output — canonical, capability-gated notification delivery.
14030
+ *
14031
+ * Apprise-derived model (see
14032
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
14033
+ * callers emit ONE canonical `Notification`; each provider declares a
14034
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
14035
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
14036
+ * message to what the kind supports — callers never special-case a service.
14037
+ *
14038
+ * DESIGN DECISIONS (locked):
14039
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
14040
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
14041
+ * cap. Rationale: the admin UI needs one uniform surface across the
14042
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
14043
+ * alternative would fork the UI per addon and cannot host the
14044
+ * discovery→adopt flow.
14045
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
14046
+ * the generated cap-mount auto-`concatCollection`-fans them across every
14047
+ * registered provider (notifiers addon + HA addon) so one catalog is
14048
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
14049
+ * `addonId` the generated collection router extracts from the call input.
14050
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
14051
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
14052
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
14053
+ * base64 fallback needed.
14054
+ *
14055
+ * TODO (deferred, closed-set change — separate decision): add
14056
+ * `providerKind: 'notify'` so notification providers surface on the unified
14057
+ * admin "Integrations" page.
14058
+ */
14059
+ /**
14060
+ * Zentik-derived typed-media enum — the superset across every kind. Each
14061
+ * adapter picks what it supports and the degrade engine filters the rest.
14062
+ */
14063
+ var AttachmentMediaTypeSchema = _enum([
14064
+ "image",
14065
+ "video",
14066
+ "gif",
14067
+ "audio",
14068
+ "icon"
14069
+ ]);
14070
+ /**
14071
+ * A single attachment. Exactly one of `url` (remote source, most adapters
14072
+ * prefer this) or `bytes` (inline source; required for Pushover-style
14073
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
14074
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
14075
+ */
14076
+ var AttachmentSchema = object({
14077
+ mediaType: AttachmentMediaTypeSchema,
14078
+ url: string().optional(),
14079
+ bytes: _instanceof(Uint8Array).optional(),
14080
+ mime: string().optional(),
14081
+ name: string().optional()
14082
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
14083
+ var NotificationFormatSchema = _enum([
14084
+ "text",
14085
+ "markdown",
14086
+ "html"
14087
+ ]);
14088
+ /**
14089
+ * The CLOSED icon vocabulary an action button may use.
14090
+ *
14091
+ * A closed set, not a free string, and that is the whole point: an arbitrary
14092
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
14093
+ * notices — the same class of gap as a zone vocabulary nothing produced
14094
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
14095
+ * declares `actionIcons: false` and the degrade engine strips the field.
14096
+ *
14097
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
14098
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
14099
+ * differently.
14100
+ *
14101
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
14102
+ *
14103
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
14104
+ * the whole set onto its own renderer's vocabulary through a
14105
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
14106
+ * fallback, so adding a member here fails every adapter's build until someone
14107
+ * decides its glyph, which is the only place that decision can be made
14108
+ * honestly.
14109
+ *
14110
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
14111
+ * `disarm` straight through; iOS feeds that string to
14112
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
14113
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
14114
+ * mapping, and "the field is documented" is not "the value renders".
14115
+ *
14116
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
14117
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
14118
+ * so an addon that emits a token the running hub does not know does not lose an
14119
+ * icon — its whole `send` fails Zod validation and the notification never
14120
+ * arrives. Never emit a new token from an addon before the train carrying it.
14121
+ */
14122
+ var NotificationActionIconSchema = _enum([
14123
+ "acknowledge",
14124
+ "dismiss",
14125
+ "silence",
14126
+ "snooze",
14127
+ "view",
14128
+ "play",
14129
+ "open",
14130
+ "close",
14131
+ "lock",
14132
+ "unlock",
14133
+ "arm",
14134
+ "arm-home",
14135
+ "arm-away",
14136
+ "arm-night",
14137
+ "disarm",
14138
+ "light",
14139
+ "alert",
14140
+ "camera"
14141
+ ]);
14142
+ /** A single tap-through action button. */
14143
+ var NotificationActionSchema = object({
14144
+ id: string(),
14145
+ label: string(),
14146
+ url: string().optional(),
14147
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
14148
+ icon: NotificationActionIconSchema.optional(),
14149
+ /**
14150
+ * Renders in a warning style where the notifier supports it.
14151
+ *
14152
+ * A HINT, never a gate. The callback's authority is its token and nothing
14153
+ * else — see `notification-center/action-token.ts` for what that does and
14154
+ * does not buy.
14155
+ */
14156
+ destructive: boolean().optional(),
14157
+ /**
14158
+ * How the tap should REACH the url.
14159
+ *
14160
+ * `navigate` (absent, and every button authored before this field) opens it:
14161
+ * the phone leaves the notification and shows whatever the callback returns.
14162
+ * That is right for a button whose answer the operator wants to read.
14163
+ *
14164
+ * `background` fires it as a POST and stays put. It exists for the buttons
14165
+ * whose whole point is not to interrupt — "silence this for 30 minutes" is
14166
+ * an answer to the notification, and being thrown into a browser tab to
14167
+ * confirm it costs more attention than the notification did. A backend that
14168
+ * cannot do a background call renders it as an ordinary link (the adapters
14169
+ * fall back rather than dropping the button), so this is a preference, never
14170
+ * a requirement.
14171
+ */
14172
+ mode: _enum(["navigate", "background"]).optional()
14173
+ });
14174
+ /** A delivery parameter value: what a notifier declared it takes. */
14175
+ var DeliveryParamValueSchema = union([
14176
+ string(),
14177
+ number(),
14178
+ boolean()
14179
+ ]);
14180
+ /**
14181
+ * One extra delivery parameter a notifier KIND declares (D938). A rule that
14182
+ * addresses a target of that kind may set it; a kind that declares nothing is
14183
+ * never handed one. The admin UI derives its control from this descriptor —
14184
+ * no editor knows a service by name.
14185
+ *
14186
+ * Optional fields are `.optional()`, never `.default()` (a Zod default does not
14187
+ * run on the addon cap path).
14188
+ */
14189
+ var DeliveryParamSchema = object({
14190
+ key: string().min(1),
14191
+ label: string(),
14192
+ /** i18n key of the label, resolved by the declaring addon's namespace. */
14193
+ labelKey: string().optional(),
14194
+ type: _enum([
14195
+ "enum",
14196
+ "number",
14197
+ "boolean",
14198
+ "string"
14199
+ ]),
14200
+ /** `enum` only: the closed set of accepted values. */
14201
+ options: array(object({
14202
+ value: string(),
14203
+ label: string()
14204
+ })).optional(),
14205
+ /** `number` only. */
14206
+ min: number().optional(),
14207
+ max: number().optional(),
14208
+ /** What the notifier does when the rule sets nothing; shown as the placeholder. */
14209
+ default: DeliveryParamValueSchema.optional()
14210
+ });
14211
+ /**
14212
+ * The canonical notification. `body` is the only hard field (Apprise model).
14213
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
14214
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
14215
+ * the adapter maps this ordinal onto its native level. `level?` is an
14216
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
14217
+ * `priority` for that one target.
14218
+ */
14219
+ var NotificationSchema = object({
14220
+ body: string(),
14221
+ title: string().optional(),
14222
+ format: NotificationFormatSchema.default("text"),
14223
+ priority: number().int().min(1).max(5).default(3),
14224
+ level: string().optional(),
14225
+ attachments: array(AttachmentSchema).optional(),
14226
+ clickUrl: string().optional(),
14227
+ actions: array(NotificationActionSchema).optional(),
14228
+ sound: string().optional(),
14229
+ ttl: number().optional(),
14230
+ tag: string().optional(),
14231
+ deviceId: number().optional(),
14232
+ eventId: string().optional(),
14233
+ metadata: record(string(), unknown()).optional(),
14234
+ /**
14235
+ * Values for the parameters the target's kind DECLARES in
14236
+ * {@link TargetKindSchema} `deliveryParams`. Already validated against the
14237
+ * declaration by the sender; a notifier reads only the keys it declared.
14238
+ */
14239
+ delivery: record(string(), DeliveryParamValueSchema).optional()
14240
+ });
14241
+ /** One declared native severity/priority level for a kind. */
14242
+ var TargetKindLevelSchema = object({
14243
+ id: string(),
14244
+ label: string(),
14245
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
14246
+ ordinal: number().int().min(1).max(5).nullable(),
14247
+ flags: object({
14248
+ critical: boolean().optional(),
14249
+ silent: boolean().optional(),
14250
+ noPush: boolean().optional()
14251
+ }).optional(),
14252
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
14253
+ requires: array(string()).optional(),
14254
+ description: string().optional()
14255
+ });
14256
+ /** The full capability block consulted before dispatch. */
14257
+ var TargetKindCapsSchema = object({
14258
+ attachments: object({
14259
+ mediaTypes: array(AttachmentMediaTypeSchema),
14260
+ mode: _enum([
14261
+ "url",
14262
+ "bytes",
14263
+ "both"
14264
+ ]),
14265
+ max: number().int().nonnegative(),
14266
+ maxBytes: number().int().positive().optional()
14267
+ }),
14268
+ /** Max action buttons (0 = none). */
14269
+ actions: number().int().nonnegative(),
14270
+ /**
14271
+ * Whether this kind renders a per-action ICON.
14272
+ *
14273
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
14274
+ * run on the addon cap path — three production failures in one day taught
14275
+ * this repo that once. Absent is read as false by the degrade engine, which
14276
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
14277
+ * assumed and dropped costs the operator's trust in the field.
14278
+ */
14279
+ actionIcons: boolean().optional(),
14280
+ levels: array(TargetKindLevelSchema),
14281
+ format: array(NotificationFormatSchema),
14282
+ clickUrl: boolean(),
14283
+ sound: boolean(),
14284
+ ttl: boolean(),
14285
+ bodyMaxLen: number().int().positive()
14286
+ });
14287
+ /**
14288
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
14289
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
14290
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
14291
+ * the union is large and not meant for runtime validation here; the exported
14292
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
14293
+ */
14294
+ var ConfigSchemaPassthrough$4 = unknown();
14295
+ var TargetKindSchema = object({
14296
+ kind: string(),
14297
+ label: string(),
14298
+ icon: string(),
14299
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
14300
+ addonId: string(),
14301
+ /**
14302
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
14303
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
14304
+ * when the addon bundles no icon for that kind — the client then falls back
14305
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
14306
+ *
14307
+ * Root-relative on purpose: it resolves against whatever origin serves a web
14308
+ * client, and a native client joins it onto its own hub base.
14309
+ *
14310
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
14311
+ * field that survived only because the runtime cap-router forwards provider
14312
+ * output verbatim — so every consumer had to re-declare it by hand to stop
14313
+ * its own Zod parse from stripping it, and the whole arrangement would have
14314
+ * broken silently the moment output validation was tightened anywhere.
14315
+ */
14316
+ iconUrl: string().optional(),
14317
+ /**
14318
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
14319
+ *
14320
+ * The server knows this and therefore says it, because the client cannot
14321
+ * safely guess: a React-Native client renders SVG and raster through two
14322
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
14323
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
14324
+ * placeholder glyph for every vector icon while the web build looked fine.
14325
+ *
14326
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
14327
+ * not been updated — a client that cannot determine the type should prefer
14328
+ * its raster path, which is the safe default for an unknown image.
14329
+ */
14330
+ iconMediaType: string().optional(),
14331
+ configSchema: ConfigSchemaPassthrough$4,
14332
+ /**
14333
+ * The config keys that decide WHERE this kind's secrets are sent (server,
14334
+ * base URL, instance, chat, recipient). A save that changes any of them
14335
+ * while leaving a secret redacted is refused by the provider (D634), so an
14336
+ * editor uses this to ask for the secret on exactly those edits instead of
14337
+ * on every edit.
14338
+ *
14339
+ * Optional: absent from a provider that holds no secret (the Home Assistant
14340
+ * kinds) and from a provider older than this field — an editor that finds
14341
+ * it absent must assume ANY edit may need the secret.
14342
+ */
14343
+ destinationKeys: array(string()).optional(),
14344
+ supportsDiscovery: boolean(),
14345
+ /**
14346
+ * Extra delivery parameters a rule may set for a target of this kind (D938).
14347
+ * Absent = none; the rule editor then offers nothing.
14348
+ */
14349
+ deliveryParams: array(DeliveryParamSchema).optional(),
14350
+ caps: TargetKindCapsSchema
14351
+ });
14352
+ /**
14353
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
14354
+ * (return a presence marker only) when serving `listTargets` — never
14355
+ * round-trip a stored secret to the UI.
14356
+ */
14357
+ var TargetSchema = object({
14358
+ id: string(),
14359
+ name: string(),
14360
+ kind: string(),
14361
+ addonId: string(),
14362
+ enabled: boolean(),
14363
+ config: record(string(), unknown())
14364
+ });
14365
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
14366
+ var DiscoveredTargetSchema = object({
14367
+ kind: string(),
14368
+ suggestedName: string(),
14369
+ config: record(string(), unknown())
14370
+ });
14371
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
14372
+ var RenderedAsSchema = object({
14373
+ level: string(),
14374
+ format: NotificationFormatSchema,
14375
+ attachmentsSent: number().int().nonnegative(),
14376
+ actionsSent: number().int().nonnegative(),
14377
+ truncated: boolean(),
14378
+ dropped: array(string())
14379
+ });
14380
+ var SendResultSchema = object({
14381
+ success: boolean(),
14382
+ error: string().optional(),
14383
+ renderedAs: RenderedAsSchema.optional()
14384
+ });
14385
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
14386
+ var TestResultSchema = SendResultSchema;
14387
+ /**
14388
+ * Pre-save credential probe against a partial (unsaved) target config.
14389
+ * `resolvedUrl` never includes secrets (token stripped from path).
14390
+ */
14391
+ var ProbeTargetConfigResultSchema = object({
14392
+ ok: boolean(),
14393
+ resolvedUrl: string(),
14394
+ httpStatus: number().int().optional(),
14395
+ stateInstance: string().optional(),
14396
+ error: string().optional()
14397
+ });
14398
+ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
14399
+ kind: string(),
14400
+ config: record(string(), unknown()).optional(),
14401
+ /**
14402
+ * When editing / duplicating, the blank password field never round-trips
14403
+ * the stored token. Pass the source target id so the provider can restore
14404
+ * secrets server-side before dialling (same merge as upsert).
14405
+ */
14406
+ targetId: string().optional()
14407
+ }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
14408
+ kind: string(),
14409
+ config: record(string(), unknown()),
14410
+ /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
14411
+ targetId: string().optional()
14412
+ }), ProbeTargetConfigResultSchema, {
14413
+ kind: "mutation",
14414
+ auth: "admin"
14415
+ }), method(object({
14416
+ targetId: string(),
14417
+ notification: NotificationSchema
14418
+ }), SendResultSchema, {
14419
+ kind: "mutation",
14420
+ auth: "admin"
14421
+ }), method(object({
14422
+ targetId: string(),
14423
+ sample: NotificationSchema.optional()
14424
+ }), TestResultSchema, {
14425
+ kind: "mutation",
14426
+ auth: "admin"
14427
+ }), method(object({
14428
+ target: TargetSchema,
14429
+ /**
14430
+ * Duplicate flow: create a NEW target id while copying secrets from an
14431
+ * existing row. The UI never holds the stored token; blank / redacted
14432
+ * password fields are restored from this source (D634 still applies if
14433
+ * destinations diverge without a re-typed secret).
14434
+ */
14435
+ cloneSecretsFrom: string().optional()
14436
+ }), TargetSchema, {
14437
+ kind: "mutation",
14438
+ auth: "admin"
14439
+ }), method(object({ targetId: string() }), _void(), {
14440
+ kind: "mutation",
14441
+ auth: "admin"
14442
+ }), method(object({
14443
+ targetId: string(),
14444
+ enabled: boolean()
14445
+ }), _void(), {
14446
+ kind: "mutation",
14447
+ auth: "admin"
14448
+ });
14449
+ looseObject({
14450
+ name: string().min(1),
14451
+ enabled: boolean().optional()
14452
+ });
14453
+ TargetSchema.omit({ id: true });
13986
14454
  var EncodeProfileSchema = object({
13987
14455
  video: object({
13988
14456
  codec: _enum([
@@ -22200,7 +22668,6 @@ var SYSTEM_CAP_VOCABULARY = {
22200
22668
  "calendar-source": system(INTERNAL),
22201
22669
  calendars: system("calendars feed schedules and conditions, not device actions"),
22202
22670
  composer: system("device composition — an authoring tool, not an action"),
22203
- "connection-test": system(INTERNAL),
22204
22671
  "core-blocks": system(INTERNAL),
22205
22672
  "custom-model-registry": system(ML),
22206
22673
  "data-store-provider": system(INTERNAL),
@@ -44117,427 +44584,6 @@ var UnitWarningSchema = object({
44117
44584
  assumedUnit: string(),
44118
44585
  message: string()
44119
44586
  });
44120
- /**
44121
- * notification-output — canonical, capability-gated notification delivery.
44122
- *
44123
- * Apprise-derived model (see
44124
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
44125
- * callers emit ONE canonical `Notification`; each provider declares a
44126
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
44127
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
44128
- * message to what the kind supports — callers never special-case a service.
44129
- *
44130
- * DESIGN DECISIONS (locked):
44131
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
44132
- * `setTargetEnabled`), each provider persisting via the `settings-store`
44133
- * cap. Rationale: the admin UI needs one uniform surface across the
44134
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
44135
- * alternative would fork the UI per addon and cannot host the
44136
- * discovery→adopt flow.
44137
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
44138
- * the generated cap-mount auto-`concatCollection`-fans them across every
44139
- * registered provider (notifiers addon + HA addon) so one catalog is
44140
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
44141
- * `addonId` the generated collection router extracts from the call input.
44142
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
44143
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
44144
- * `storage` / `storage-provider` / `recording` caps over the same path. No
44145
- * base64 fallback needed.
44146
- *
44147
- * TODO (deferred, closed-set change — separate decision): add
44148
- * `providerKind: 'notify'` so notification providers surface on the unified
44149
- * admin "Integrations" page.
44150
- */
44151
- /**
44152
- * Zentik-derived typed-media enum — the superset across every kind. Each
44153
- * adapter picks what it supports and the degrade engine filters the rest.
44154
- */
44155
- var AttachmentMediaTypeSchema = _enum([
44156
- "image",
44157
- "video",
44158
- "gif",
44159
- "audio",
44160
- "icon"
44161
- ]);
44162
- /**
44163
- * A single attachment. Exactly one of `url` (remote source, most adapters
44164
- * prefer this) or `bytes` (inline source; required for Pushover-style
44165
- * bytes-only kinds) MUST be present — the degrade engine expresses a
44166
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
44167
- */
44168
- var AttachmentSchema = object({
44169
- mediaType: AttachmentMediaTypeSchema,
44170
- url: string().optional(),
44171
- bytes: _instanceof(Uint8Array).optional(),
44172
- mime: string().optional(),
44173
- name: string().optional()
44174
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
44175
- var NotificationFormatSchema = _enum([
44176
- "text",
44177
- "markdown",
44178
- "html"
44179
- ]);
44180
- /**
44181
- * The CLOSED icon vocabulary an action button may use.
44182
- *
44183
- * A closed set, not a free string, and that is the whole point: an arbitrary
44184
- * icon name is one that ntfy renders, zentik silently drops, and nobody
44185
- * notices — the same class of gap as a zone vocabulary nothing produced
44186
- * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
44187
- * declares `actionIcons: false` and the degrade engine strips the field.
44188
- *
44189
- * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
44190
- * renderer's icon set; "acknowledge" survives an adapter that draws it
44191
- * differently.
44192
- *
44193
- * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
44194
- *
44195
- * These names are for US. **No adapter may forward one verbatim.** Each maps
44196
- * the whole set onto its own renderer's vocabulary through a
44197
- * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
44198
- * fallback, so adding a member here fails every adapter's build until someone
44199
- * decides its glyph, which is the only place that decision can be made
44200
- * honestly.
44201
- *
44202
- * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
44203
- * `disarm` straight through; iOS feeds that string to
44204
- * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
44205
- * and every snooze and alarm button arrived BLANK. A pass-through is not a
44206
- * mapping, and "the field is documented" is not "the value renders".
44207
- *
44208
- * Adding a member is TRAIN-BOUND. The enum lives in the published
44209
- * `@camstack/server` closure and the cap seam validates against the HUB's copy,
44210
- * so an addon that emits a token the running hub does not know does not lose an
44211
- * icon — its whole `send` fails Zod validation and the notification never
44212
- * arrives. Never emit a new token from an addon before the train carrying it.
44213
- */
44214
- var NotificationActionIconSchema = _enum([
44215
- "acknowledge",
44216
- "dismiss",
44217
- "silence",
44218
- "snooze",
44219
- "view",
44220
- "play",
44221
- "open",
44222
- "close",
44223
- "lock",
44224
- "unlock",
44225
- "arm",
44226
- "arm-home",
44227
- "arm-away",
44228
- "arm-night",
44229
- "disarm",
44230
- "light",
44231
- "alert",
44232
- "camera"
44233
- ]);
44234
- /** A single tap-through action button. */
44235
- var NotificationActionSchema = object({
44236
- id: string(),
44237
- label: string(),
44238
- url: string().optional(),
44239
- /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
44240
- icon: NotificationActionIconSchema.optional(),
44241
- /**
44242
- * Renders in a warning style where the notifier supports it.
44243
- *
44244
- * A HINT, never a gate. The callback's authority is its token and nothing
44245
- * else — see `notification-center/action-token.ts` for what that does and
44246
- * does not buy.
44247
- */
44248
- destructive: boolean().optional(),
44249
- /**
44250
- * How the tap should REACH the url.
44251
- *
44252
- * `navigate` (absent, and every button authored before this field) opens it:
44253
- * the phone leaves the notification and shows whatever the callback returns.
44254
- * That is right for a button whose answer the operator wants to read.
44255
- *
44256
- * `background` fires it as a POST and stays put. It exists for the buttons
44257
- * whose whole point is not to interrupt — "silence this for 30 minutes" is
44258
- * an answer to the notification, and being thrown into a browser tab to
44259
- * confirm it costs more attention than the notification did. A backend that
44260
- * cannot do a background call renders it as an ordinary link (the adapters
44261
- * fall back rather than dropping the button), so this is a preference, never
44262
- * a requirement.
44263
- */
44264
- mode: _enum(["navigate", "background"]).optional()
44265
- });
44266
- /** A delivery parameter value: what a notifier declared it takes. */
44267
- var DeliveryParamValueSchema = union([
44268
- string(),
44269
- number(),
44270
- boolean()
44271
- ]);
44272
- /**
44273
- * One extra delivery parameter a notifier KIND declares (D938). A rule that
44274
- * addresses a target of that kind may set it; a kind that declares nothing is
44275
- * never handed one. The admin UI derives its control from this descriptor —
44276
- * no editor knows a service by name.
44277
- *
44278
- * Optional fields are `.optional()`, never `.default()` (a Zod default does not
44279
- * run on the addon cap path).
44280
- */
44281
- var DeliveryParamSchema = object({
44282
- key: string().min(1),
44283
- label: string(),
44284
- /** i18n key of the label, resolved by the declaring addon's namespace. */
44285
- labelKey: string().optional(),
44286
- type: _enum([
44287
- "enum",
44288
- "number",
44289
- "boolean",
44290
- "string"
44291
- ]),
44292
- /** `enum` only: the closed set of accepted values. */
44293
- options: array(object({
44294
- value: string(),
44295
- label: string()
44296
- })).optional(),
44297
- /** `number` only. */
44298
- min: number().optional(),
44299
- max: number().optional(),
44300
- /** What the notifier does when the rule sets nothing; shown as the placeholder. */
44301
- default: DeliveryParamValueSchema.optional()
44302
- });
44303
- /**
44304
- * The canonical notification. `body` is the only hard field (Apprise model).
44305
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
44306
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
44307
- * the adapter maps this ordinal onto its native level. `level?` is an
44308
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
44309
- * `priority` for that one target.
44310
- */
44311
- var NotificationSchema = object({
44312
- body: string(),
44313
- title: string().optional(),
44314
- format: NotificationFormatSchema.default("text"),
44315
- priority: number().int().min(1).max(5).default(3),
44316
- level: string().optional(),
44317
- attachments: array(AttachmentSchema).optional(),
44318
- clickUrl: string().optional(),
44319
- actions: array(NotificationActionSchema).optional(),
44320
- sound: string().optional(),
44321
- ttl: number().optional(),
44322
- tag: string().optional(),
44323
- deviceId: number().optional(),
44324
- eventId: string().optional(),
44325
- metadata: record(string(), unknown()).optional(),
44326
- /**
44327
- * Values for the parameters the target's kind DECLARES in
44328
- * {@link TargetKindSchema} `deliveryParams`. Already validated against the
44329
- * declaration by the sender; a notifier reads only the keys it declared.
44330
- */
44331
- delivery: record(string(), DeliveryParamValueSchema).optional()
44332
- });
44333
- /** One declared native severity/priority level for a kind. */
44334
- var TargetKindLevelSchema = object({
44335
- id: string(),
44336
- label: string(),
44337
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
44338
- ordinal: number().int().min(1).max(5).nullable(),
44339
- flags: object({
44340
- critical: boolean().optional(),
44341
- silent: boolean().optional(),
44342
- noPush: boolean().optional()
44343
- }).optional(),
44344
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
44345
- requires: array(string()).optional(),
44346
- description: string().optional()
44347
- });
44348
- /** The full capability block consulted before dispatch. */
44349
- var TargetKindCapsSchema = object({
44350
- attachments: object({
44351
- mediaTypes: array(AttachmentMediaTypeSchema),
44352
- mode: _enum([
44353
- "url",
44354
- "bytes",
44355
- "both"
44356
- ]),
44357
- max: number().int().nonnegative(),
44358
- maxBytes: number().int().positive().optional()
44359
- }),
44360
- /** Max action buttons (0 = none). */
44361
- actions: number().int().nonnegative(),
44362
- /**
44363
- * Whether this kind renders a per-action ICON.
44364
- *
44365
- * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
44366
- * run on the addon cap path — three production failures in one day taught
44367
- * this repo that once. Absent is read as false by the degrade engine, which
44368
- * is the safe direction: an icon that is not rendered costs nothing, an icon
44369
- * assumed and dropped costs the operator's trust in the field.
44370
- */
44371
- actionIcons: boolean().optional(),
44372
- levels: array(TargetKindLevelSchema),
44373
- format: array(NotificationFormatSchema),
44374
- clickUrl: boolean(),
44375
- sound: boolean(),
44376
- ttl: boolean(),
44377
- bodyMaxLen: number().int().positive()
44378
- });
44379
- /**
44380
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
44381
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
44382
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
44383
- * the union is large and not meant for runtime validation here; the exported
44384
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
44385
- */
44386
- var ConfigSchemaPassthrough$4 = unknown();
44387
- var TargetKindSchema = object({
44388
- kind: string(),
44389
- label: string(),
44390
- icon: string(),
44391
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
44392
- addonId: string(),
44393
- /**
44394
- * URL of the kind's bundled BRAND icon, served by the providing addon over
44395
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
44396
- * when the addon bundles no icon for that kind — the client then falls back
44397
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
44398
- *
44399
- * Root-relative on purpose: it resolves against whatever origin serves a web
44400
- * client, and a native client joins it onto its own hub base.
44401
- *
44402
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
44403
- * field that survived only because the runtime cap-router forwards provider
44404
- * output verbatim — so every consumer had to re-declare it by hand to stop
44405
- * its own Zod parse from stripping it, and the whole arrangement would have
44406
- * broken silently the moment output validation was tightened anywhere.
44407
- */
44408
- iconUrl: string().optional(),
44409
- /**
44410
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
44411
- *
44412
- * The server knows this and therefore says it, because the client cannot
44413
- * safely guess: a React-Native client renders SVG and raster through two
44414
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
44415
- * not decode SVG on iOS/Android), so without this it silently fell back to a
44416
- * placeholder glyph for every vector icon while the web build looked fine.
44417
- *
44418
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
44419
- * not been updated — a client that cannot determine the type should prefer
44420
- * its raster path, which is the safe default for an unknown image.
44421
- */
44422
- iconMediaType: string().optional(),
44423
- configSchema: ConfigSchemaPassthrough$4,
44424
- /**
44425
- * The config keys that decide WHERE this kind's secrets are sent (server,
44426
- * base URL, instance, chat, recipient). A save that changes any of them
44427
- * while leaving a secret redacted is refused by the provider (D634), so an
44428
- * editor uses this to ask for the secret on exactly those edits instead of
44429
- * on every edit.
44430
- *
44431
- * Optional: absent from a provider that holds no secret (the Home Assistant
44432
- * kinds) and from a provider older than this field — an editor that finds
44433
- * it absent must assume ANY edit may need the secret.
44434
- */
44435
- destinationKeys: array(string()).optional(),
44436
- supportsDiscovery: boolean(),
44437
- /**
44438
- * Extra delivery parameters a rule may set for a target of this kind (D938).
44439
- * Absent = none; the rule editor then offers nothing.
44440
- */
44441
- deliveryParams: array(DeliveryParamSchema).optional(),
44442
- caps: TargetKindCapsSchema
44443
- });
44444
- /**
44445
- * A persisted target. `config` holds secrets; providers REDACT secret fields
44446
- * (return a presence marker only) when serving `listTargets` — never
44447
- * round-trip a stored secret to the UI.
44448
- */
44449
- var TargetSchema = object({
44450
- id: string(),
44451
- name: string(),
44452
- kind: string(),
44453
- addonId: string(),
44454
- enabled: boolean(),
44455
- config: record(string(), unknown())
44456
- });
44457
- /** A discovery-surfaced candidate (config is partial + non-secret). */
44458
- var DiscoveredTargetSchema = object({
44459
- kind: string(),
44460
- suggestedName: string(),
44461
- config: record(string(), unknown())
44462
- });
44463
- /** The degrade engine's report — what was resolved / dropped / degraded. */
44464
- var RenderedAsSchema = object({
44465
- level: string(),
44466
- format: NotificationFormatSchema,
44467
- attachmentsSent: number().int().nonnegative(),
44468
- actionsSent: number().int().nonnegative(),
44469
- truncated: boolean(),
44470
- dropped: array(string())
44471
- });
44472
- var SendResultSchema = object({
44473
- success: boolean(),
44474
- error: string().optional(),
44475
- renderedAs: RenderedAsSchema.optional()
44476
- });
44477
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
44478
- var TestResultSchema = SendResultSchema;
44479
- /**
44480
- * Pre-save credential probe against a partial (unsaved) target config.
44481
- * `resolvedUrl` never includes secrets (token stripped from path).
44482
- */
44483
- var ProbeTargetConfigResultSchema = object({
44484
- ok: boolean(),
44485
- resolvedUrl: string(),
44486
- httpStatus: number().int().optional(),
44487
- stateInstance: string().optional(),
44488
- error: string().optional()
44489
- });
44490
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
44491
- kind: string(),
44492
- config: record(string(), unknown()).optional(),
44493
- /**
44494
- * When editing / duplicating, the blank password field never round-trips
44495
- * the stored token. Pass the source target id so the provider can restore
44496
- * secrets server-side before dialling (same merge as upsert).
44497
- */
44498
- targetId: string().optional()
44499
- }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
44500
- kind: string(),
44501
- config: record(string(), unknown()),
44502
- /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
44503
- targetId: string().optional()
44504
- }), ProbeTargetConfigResultSchema, {
44505
- kind: "mutation",
44506
- auth: "admin"
44507
- }), method(object({
44508
- targetId: string(),
44509
- notification: NotificationSchema
44510
- }), SendResultSchema, {
44511
- kind: "mutation",
44512
- auth: "admin"
44513
- }), method(object({
44514
- targetId: string(),
44515
- sample: NotificationSchema.optional()
44516
- }), TestResultSchema, {
44517
- kind: "mutation",
44518
- auth: "admin"
44519
- }), method(object({
44520
- target: TargetSchema,
44521
- /**
44522
- * Duplicate flow: create a NEW target id while copying secrets from an
44523
- * existing row. The UI never holds the stored token; blank / redacted
44524
- * password fields are restored from this source (D634 still applies if
44525
- * destinations diverge without a re-typed secret).
44526
- */
44527
- cloneSecretsFrom: string().optional()
44528
- }), TargetSchema, {
44529
- kind: "mutation",
44530
- auth: "admin"
44531
- }), method(object({ targetId: string() }), _void(), {
44532
- kind: "mutation",
44533
- auth: "admin"
44534
- }), method(object({
44535
- targetId: string(),
44536
- enabled: boolean()
44537
- }), _void(), {
44538
- kind: "mutation",
44539
- auth: "admin"
44540
- });
44541
44587
  _enum([
44542
44588
  "automation",
44543
44589
  "action",
@@ -48415,37 +48461,6 @@ method(object({
48415
48461
  kind: "mutation",
48416
48462
  auth: "admin"
48417
48463
  }), method(object({ blockIds: array(string()) }), object({ states: array(CompositionBlockStateSchema) }), { auth: "admin" });
48418
- var ConnectionTestOutcomeSchema = discriminatedUnion("outcome", [
48419
- object({
48420
- outcome: literal("validated"),
48421
- /** Round-trip of the sign-in, when the provider measured it. */
48422
- latencyMs: number().nonnegative().optional(),
48423
- /** Optional human detail worth showing next to the tick
48424
- * ("3 devices visible on this account"). */
48425
- detail: string().optional()
48426
- }).strict(),
48427
- object({
48428
- outcome: literal("rejected"),
48429
- error: string()
48430
- }).strict(),
48431
- object({
48432
- outcome: literal("inconclusive"),
48433
- error: string()
48434
- }).strict()
48435
- ]);
48436
- var ConnectionTestInputSchema = object({
48437
- /** Candidate integration settings, exactly as the create form collected them. */
48438
- settings: record(string(), unknown()) });
48439
- /**
48440
- * What the provider's test actually DOES, so the UI can say it in words before
48441
- * the operator presses the button ("Signs in to the Dreo cloud"). Purely
48442
- * descriptive — it never changes routing.
48443
- */
48444
- var ConnectionTestDescriptorSchema = object({ label: string() });
48445
- method(ConnectionTestInputSchema, ConnectionTestOutcomeSchema, {
48446
- kind: "mutation",
48447
- auth: "admin"
48448
- }), method(_void(), ConnectionTestDescriptorSchema, { auth: "admin" });
48449
48464
  object({
48450
48465
  /** True when the upstream system considers the entity connected. */
48451
48466
  connected: boolean(),
@@ -54299,10 +54314,9 @@ var AvailableIntegrationTypeSchema = object({
54299
54314
  * locations" checkbox. Provider-declared in the addon manifest. */
54300
54315
  supportsLocationImport: boolean(),
54301
54316
  /**
54302
- * True when this integration DECLARES a pre-creation test (the
54303
- * `connection-test` cap, or a broker whose settings it stores). Drives the
54304
- * Test button: an integration that cannot be tested must say so up front
54305
- * rather than offering a button that always answers the same nonsense.
54317
+ * True when this integration's instances declare a Test (`integration-instances.describe().canTest`,
54318
+ * D1016). An integration that cannot be tested must say so up front rather than offering a
54319
+ * button that always answers the same nonsense.
54306
54320
  */
54307
54321
  canTest: boolean(),
54308
54322
  existingInstances: array(object({
@@ -54356,7 +54370,9 @@ var CreateIntegrationInputSchema = object({
54356
54370
  info: record(string(), unknown()).optional(),
54357
54371
  /** `stableId` of the discovery candidate the form was seeded from (D984). */
54358
54372
  originStableId: string().optional(),
54359
- settings: record(string(), unknown()).optional()
54373
+ settings: record(string(), unknown()).optional(),
54374
+ /** The instance kind being created, for a provider with several; omitted = its only testable kind. */
54375
+ kind: string().min(1).optional()
54360
54376
  });
54361
54377
  var UpdateIntegrationInputSchema = object({
54362
54378
  id: string(),
@@ -54382,13 +54398,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
54382
54398
  }), method(object({ id: string() }), DeleteIntegrationResultSchema, {
54383
54399
  kind: "mutation",
54384
54400
  auth: "admin"
54385
- }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
54386
- addonId: string(),
54387
- settings: record(string(), unknown())
54388
- }), TestConnectionResultSchema, {
54389
- kind: "mutation",
54390
- auth: "admin"
54391
- });
54401
+ }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" });
54392
54402
  /**
54393
54403
  * `integration-instances` - the connection of an addon that holds ONE or SEVERAL
54394
54404
  * (an account, a relay, a destination) as typed records (D985, D986). The addon's
@@ -62631,13 +62641,6 @@ var AddBrokerInputSchema = object({
62631
62641
  });
62632
62642
  var AddBrokerResultSchema = object({ id: string() });
62633
62643
  var IdInputSchema = object({ id: string() });
62634
- var TestResultSchema$1 = discriminatedUnion("ok", [object({
62635
- ok: literal(true),
62636
- latencyMs: number()
62637
- }), object({
62638
- ok: literal(false),
62639
- error: string()
62640
- })]);
62641
62644
  var StartEmbeddedInputSchema = object({
62642
62645
  port: number().int().min(1).max(65535).default(1883),
62643
62646
  /** Allow anonymous connect (no username/password). Default: false. */
@@ -62660,9 +62663,6 @@ method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnection
62660
62663
  }), method(IdInputSchema, _void(), {
62661
62664
  kind: "mutation",
62662
62665
  auth: "admin"
62663
- }), method(IdInputSchema, TestResultSchema$1, {
62664
- kind: "mutation",
62665
- auth: "admin"
62666
62666
  }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
62667
62667
  kind: "mutation",
62668
62668
  auth: "admin"
@@ -74287,7 +74287,7 @@ var TrainingProviderTypeSchema = object({
74287
74287
  label: string(),
74288
74288
  /** Path under the addon package (e.g. `assets/training-modal/icon.svg`). */
74289
74289
  iconAsset: string(),
74290
- /** Owning `training-provider` addon — collection selector for createEntry. */
74290
+ /** Owning `training-provider` addon. */
74291
74291
  addonId: string(),
74292
74292
  /**
74293
74293
  * Fields for one entry of this type (credentials + defaults). Absent on a
@@ -74332,36 +74332,6 @@ var TrainingProviderDescriptorSchema = object({
74332
74332
  */
74333
74333
  formValues: record(string(), unknown()).optional()
74334
74334
  });
74335
- /**
74336
- * Patch one entry's name, credentials and defaults. Pin with owning `{ addonId }`.
74337
- * An omitted or empty `tokenSecret` means KEEP the stored secret (D787).
74338
- * `{ $secretClear: true }` clears it.
74339
- */
74340
- var TrainingProviderUpdateEntryInputSchema = object({
74341
- entryId: string().min(1),
74342
- name: string().min(1),
74343
- tokenId: string().optional(),
74344
- tokenSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
74345
- environment: string().optional(),
74346
- gpuType: string().optional(),
74347
- region: string().optional(),
74348
- costCapUsd: number().nonnegative().optional(),
74349
- maxRuntimeSec: number().int().positive().optional(),
74350
- failedDatasetGraceHours: number().nonnegative().optional()
74351
- });
74352
- var TrainingCredentialCheckSchema = object({
74353
- ok: boolean(),
74354
- /** Workspace / account name, when the provider says. */
74355
- account: string().optional(),
74356
- reason: _enum([
74357
- "missing",
74358
- "invalid",
74359
- "forbidden",
74360
- "network",
74361
- "quota"
74362
- ]).optional(),
74363
- detail: string().optional()
74364
- });
74365
74335
  var ProviderJobStateSchema = _enum([
74366
74336
  "pending",
74367
74337
  "starting",
@@ -74409,22 +74379,6 @@ var TrainingLogLineSchema = object({
74409
74379
  text: string()
74410
74380
  });
74411
74381
  method(object({}), array(TrainingProviderDescriptorSchema).readonly(), { auth: "admin" }), method(object({}), array(TrainingProviderTypeSchema).readonly(), { auth: "admin" }), method(object({}), array(TrainingProviderEntrySchema).readonly(), { auth: "admin" }), method(object({
74412
- typeId: string().min(1),
74413
- name: string().min(1)
74414
- }), object({ entryId: string() }), {
74415
- kind: "mutation",
74416
- auth: "admin"
74417
- }), method(object({ entryId: string().min(1) }), object({ ok: literal(true) }), {
74418
- kind: "mutation",
74419
- auth: "admin"
74420
- }), method(TrainingProviderUpdateEntryInputSchema, object({ ok: literal(true) }), {
74421
- kind: "mutation",
74422
- auth: "admin"
74423
- }), method(object({ entryId: string().min(1) }), TrainingCredentialCheckSchema, {
74424
- kind: "mutation",
74425
- auth: "admin",
74426
- timeoutMs: 3e4
74427
- }), method(object({
74428
74382
  entryId: string().min(1),
74429
74383
  gpuType: string(),
74430
74384
  region: string(),
@@ -77810,20 +77764,6 @@ Object.freeze({
77810
77764
  access: "view",
77811
77765
  role: "admin"
77812
77766
  },
77813
- "connectionTest.describeTest": {
77814
- capName: "connection-test",
77815
- capScope: "system",
77816
- addonId: null,
77817
- access: "view",
77818
- role: "admin"
77819
- },
77820
- "connectionTest.testSettings": {
77821
- capName: "connection-test",
77822
- capScope: "system",
77823
- addonId: null,
77824
- access: "create",
77825
- role: "admin"
77826
- },
77827
77767
  "connectivity.getStatus": {
77828
77768
  capName: "connectivity",
77829
77769
  capScope: "device",
@@ -80190,13 +80130,6 @@ Object.freeze({
80190
80130
  access: "view",
80191
80131
  role: "admin"
80192
80132
  },
80193
- "integrations.testConnection": {
80194
- capName: "integrations",
80195
- capScope: "system",
80196
- addonId: null,
80197
- access: "create",
80198
- role: "admin"
80199
- },
80200
80133
  "integrations.update": {
80201
80134
  capName: "integrations",
80202
80135
  capScope: "system",
@@ -81429,13 +81362,6 @@ Object.freeze({
81429
81362
  access: "create",
81430
81363
  role: "admin"
81431
81364
  },
81432
- "mqttBroker.testConnection": {
81433
- capName: "mqtt-broker",
81434
- capScope: "system",
81435
- addonId: null,
81436
- access: "create",
81437
- role: "admin"
81438
- },
81439
81365
  "nativeObjectDetection.getOptions": {
81440
81366
  capName: "native-object-detection",
81441
81367
  capScope: "device",
@@ -86266,20 +86192,6 @@ Object.freeze({
86266
86192
  access: "create",
86267
86193
  role: "admin"
86268
86194
  },
86269
- "trainingProvider.createEntry": {
86270
- capName: "training-provider",
86271
- capScope: "system",
86272
- addonId: null,
86273
- access: "create",
86274
- role: "admin"
86275
- },
86276
- "trainingProvider.deleteEntry": {
86277
- capName: "training-provider",
86278
- capScope: "system",
86279
- addonId: null,
86280
- access: "delete",
86281
- role: "admin"
86282
- },
86283
86195
  "trainingProvider.describe": {
86284
86196
  capName: "training-provider",
86285
86197
  capScope: "system",
@@ -86350,13 +86262,6 @@ Object.freeze({
86350
86262
  access: "create",
86351
86263
  role: "admin"
86352
86264
  },
86353
- "trainingProvider.updateEntry": {
86354
- capName: "training-provider",
86355
- capScope: "system",
86356
- addonId: null,
86357
- access: "create",
86358
- role: "admin"
86359
- },
86360
86265
  "trainingProvider.uploadDataset": {
86361
86266
  capName: "training-provider",
86362
86267
  capScope: "system",
@@ -86364,13 +86269,6 @@ Object.freeze({
86364
86269
  access: "create",
86365
86270
  role: "admin"
86366
86271
  },
86367
- "trainingProvider.validateCredentials": {
86368
- capName: "training-provider",
86369
- capScope: "system",
86370
- addonId: null,
86371
- access: "create",
86372
- role: "admin"
86373
- },
86374
86272
  "turnProvider.getTurnServers": {
86375
86273
  capName: "turn-provider",
86376
86274
  capScope: "system",