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