@camstack/addon-provider-enpal 0.1.12 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/addon.js +751 -870
- package/dist/addon.mjs +751 -870
- package/package.json +2 -2
package/dist/addon.js
CHANGED
|
@@ -5981,7 +5981,7 @@ var ZodIssueCode = {
|
|
|
5981
5981
|
var ZodFirstPartyTypeKind;
|
|
5982
5982
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5983
5983
|
//#endregion
|
|
5984
|
-
//#region ../types/dist/sleep-
|
|
5984
|
+
//#region ../types/dist/sleep-D7ZKm4nb.mjs
|
|
5985
5985
|
/**
|
|
5986
5986
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5987
5987
|
* window to float samples (D455).
|
|
@@ -6750,10 +6750,33 @@ function rowsDepsOver(api) {
|
|
|
6750
6750
|
};
|
|
6751
6751
|
}
|
|
6752
6752
|
var PREFIX = "records/";
|
|
6753
|
+
/**
|
|
6754
|
+
* Cross-realm brand. An addon bundles its own copy of this module (D15) while
|
|
6755
|
+
* `ctx.records` is the HOST's door, so an error thrown by the host is an
|
|
6756
|
+
* instance of the host's classes, not of the addon's. The brand plus the code
|
|
6757
|
+
* make `instanceof` answer by identity of the failure, not of the copy.
|
|
6758
|
+
*/
|
|
6759
|
+
var RECORDS_ERROR_BRAND = Symbol.for("camstack.records-error");
|
|
6760
|
+
function carriesBrand(value) {
|
|
6761
|
+
return typeof value === "object" && value !== null && Reflect.get(value, RECORDS_ERROR_BRAND) === true;
|
|
6762
|
+
}
|
|
6753
6763
|
/** The base of every records failure. `detail` is the human part of the message. */
|
|
6754
6764
|
var RecordsError = class extends Error {
|
|
6765
|
+
/** The code a subclass stands for; the base stands for every code. */
|
|
6766
|
+
static code = void 0;
|
|
6767
|
+
/**
|
|
6768
|
+
* `instanceof` across copies of this module: any branded error is a
|
|
6769
|
+
* `RecordsError`, and is an instance of a subclass only when its code is the
|
|
6770
|
+
* subclass's code. Same-copy answers are unchanged.
|
|
6771
|
+
*/
|
|
6772
|
+
static [Symbol.hasInstance](value) {
|
|
6773
|
+
if (Function.prototype[Symbol.hasInstance].call(this, value)) return true;
|
|
6774
|
+
if (!carriesBrand(value)) return false;
|
|
6775
|
+
return this.code === void 0 || value.code === this.code;
|
|
6776
|
+
}
|
|
6755
6777
|
code;
|
|
6756
6778
|
detail;
|
|
6779
|
+
[RECORDS_ERROR_BRAND] = true;
|
|
6757
6780
|
constructor(code, detail) {
|
|
6758
6781
|
super(`${PREFIX}${code}: ${detail}`);
|
|
6759
6782
|
this.name = "RecordsError";
|
|
@@ -6762,29 +6785,33 @@ var RecordsError = class extends Error {
|
|
|
6762
6785
|
}
|
|
6763
6786
|
};
|
|
6764
6787
|
/** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
|
|
6765
|
-
var RevConflictError = class extends RecordsError {
|
|
6788
|
+
var RevConflictError = class RevConflictError extends RecordsError {
|
|
6789
|
+
static code = "rev-conflict";
|
|
6766
6790
|
constructor(detail) {
|
|
6767
|
-
super(
|
|
6791
|
+
super(RevConflictError.code, detail);
|
|
6768
6792
|
this.name = "RevConflictError";
|
|
6769
6793
|
}
|
|
6770
6794
|
};
|
|
6771
6795
|
/** The keychain could not store or serve a value: explicit, never an empty string. */
|
|
6772
|
-
var RecordSecretUnavailableError = class extends RecordsError {
|
|
6796
|
+
var RecordSecretUnavailableError = class RecordSecretUnavailableError extends RecordsError {
|
|
6797
|
+
static code = "secret-unavailable";
|
|
6773
6798
|
constructor(detail) {
|
|
6774
|
-
super(
|
|
6799
|
+
super(RecordSecretUnavailableError.code, detail);
|
|
6775
6800
|
this.name = "RecordSecretUnavailableError";
|
|
6776
6801
|
}
|
|
6777
6802
|
};
|
|
6778
|
-
var InvalidRecordError = class extends RecordsError {
|
|
6803
|
+
var InvalidRecordError = class InvalidRecordError extends RecordsError {
|
|
6804
|
+
static code = "invalid-record";
|
|
6779
6805
|
constructor(detail) {
|
|
6780
|
-
super(
|
|
6806
|
+
super(InvalidRecordError.code, detail);
|
|
6781
6807
|
this.name = "InvalidRecordError";
|
|
6782
6808
|
}
|
|
6783
6809
|
};
|
|
6784
6810
|
/** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
|
|
6785
|
-
var DeclarationRefusedError = class extends RecordsError {
|
|
6811
|
+
var DeclarationRefusedError = class DeclarationRefusedError extends RecordsError {
|
|
6812
|
+
static code = "declaration-refused";
|
|
6786
6813
|
constructor(detail) {
|
|
6787
|
-
super(
|
|
6814
|
+
super(DeclarationRefusedError.code, detail);
|
|
6788
6815
|
this.name = "DeclarationRefusedError";
|
|
6789
6816
|
}
|
|
6790
6817
|
};
|
|
@@ -9588,286 +9615,6 @@ function normalizeAddonInitResult(result) {
|
|
|
9588
9615
|
if (Array.isArray(result)) return { providers: result };
|
|
9589
9616
|
return result;
|
|
9590
9617
|
}
|
|
9591
|
-
/**
|
|
9592
|
-
* The share-view scope kinds a share-link token can carry. The hub mints
|
|
9593
|
-
* and validates them (`ShareViewGrantSchema` builds its
|
|
9594
|
-
* enum from this list); a data plane names the kinds it ACCEPTS in `share`.
|
|
9595
|
-
*/
|
|
9596
|
-
var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
|
|
9597
|
-
/** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
|
|
9598
|
-
var PeerBytesTicketSchema = object({
|
|
9599
|
-
/** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
|
|
9600
|
-
url: string().min(1),
|
|
9601
|
-
/**
|
|
9602
|
-
* The HOST node this URL means something on — the hub or a named agent,
|
|
9603
|
-
* never a runner. {@link AddonPeerBytes.open} compares it to its own and
|
|
9604
|
-
* refuses `cross-node` by name when they differ, without dialling.
|
|
9605
|
-
*/
|
|
9606
|
-
hostNodeId: string().min(1),
|
|
9607
|
-
expiresAtMs: number().int().nonnegative(),
|
|
9608
|
-
/**
|
|
9609
|
-
* What the producer DECLARED the body to be, when it knows — `null` when it
|
|
9610
|
-
* does not. Never `0` for unknown (D393): a consumer sizing a bound off this
|
|
9611
|
-
* must be able to tell "the producer did not say" from "the body is empty".
|
|
9612
|
-
*/
|
|
9613
|
-
declaredBytes: number().int().nonnegative().nullable()
|
|
9614
|
-
});
|
|
9615
|
-
function defOf(schema) {
|
|
9616
|
-
const zodBag = Reflect.get(schema, "_zod");
|
|
9617
|
-
if (typeof zodBag !== "object" || zodBag === null) return {};
|
|
9618
|
-
const def = Reflect.get(zodBag, "def");
|
|
9619
|
-
if (typeof def !== "object" || def === null) return {};
|
|
9620
|
-
return def;
|
|
9621
|
-
}
|
|
9622
|
-
function declaredHere(schema) {
|
|
9623
|
-
return globalRegistry.get(schema)?.secret === true;
|
|
9624
|
-
}
|
|
9625
|
-
/** Wrapper kinds whose `innerType` is the field itself. */
|
|
9626
|
-
var WRAPPERS = new Set([
|
|
9627
|
-
"optional",
|
|
9628
|
-
"nullable",
|
|
9629
|
-
"default",
|
|
9630
|
-
"prefault",
|
|
9631
|
-
"catch",
|
|
9632
|
-
"readonly",
|
|
9633
|
-
"nonoptional"
|
|
9634
|
-
]);
|
|
9635
|
-
/**
|
|
9636
|
-
* The schema layers that make up ONE field: the schema itself and every
|
|
9637
|
-
* wrapper below it (optional / default / nullable / readonly / catch), plus
|
|
9638
|
-
* both sides of a pipe (`z.preprocess` puts the real schema on `out`,
|
|
9639
|
-
* `.transform` on `in`).
|
|
9640
|
-
*/
|
|
9641
|
-
function layersOf(schema) {
|
|
9642
|
-
const out = [];
|
|
9643
|
-
const queue = [schema];
|
|
9644
|
-
while (queue.length > 0) {
|
|
9645
|
-
const s = queue.shift();
|
|
9646
|
-
if (!s || out.includes(s)) continue;
|
|
9647
|
-
out.push(s);
|
|
9648
|
-
const def = defOf(s);
|
|
9649
|
-
if (def.type !== void 0 && WRAPPERS.has(def.type) && def.innerType) queue.push(def.innerType);
|
|
9650
|
-
if (def.type === "pipe") {
|
|
9651
|
-
if (def.in) queue.push(def.in);
|
|
9652
|
-
if (def.out) queue.push(def.out);
|
|
9653
|
-
}
|
|
9654
|
-
}
|
|
9655
|
-
return out;
|
|
9656
|
-
}
|
|
9657
|
-
/** True when any layer of this field declares `.meta({ secret: true })`. */
|
|
9658
|
-
function isZodSecret(schema) {
|
|
9659
|
-
return layersOf(schema).some(declaredHere);
|
|
9660
|
-
}
|
|
9661
|
-
/**
|
|
9662
|
-
* Every declared secret path in a config schema: dotted keys, `[]` for "each
|
|
9663
|
-
* element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
|
|
9664
|
-
* map" (`applianceSecrets{}`). A declared
|
|
9665
|
-
* object or array is reported as one path and not descended into — everything
|
|
9666
|
-
* under it is secret.
|
|
9667
|
-
*/
|
|
9668
|
-
function collectZodSecretPaths(schema, prefix = "") {
|
|
9669
|
-
if (isZodSecret(schema)) return prefix.length > 0 ? [prefix] : [];
|
|
9670
|
-
const paths = [];
|
|
9671
|
-
for (const layer of layersOf(schema)) {
|
|
9672
|
-
const def = defOf(layer);
|
|
9673
|
-
if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
|
|
9674
|
-
else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
|
|
9675
|
-
else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
|
|
9676
|
-
else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
|
|
9677
|
-
else if (def.type === "intersection") {
|
|
9678
|
-
if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
|
|
9679
|
-
if (def.right) paths.push(...collectZodSecretPaths(def.right, prefix));
|
|
9680
|
-
}
|
|
9681
|
-
}
|
|
9682
|
-
return [...new Set(paths)];
|
|
9683
|
-
}
|
|
9684
|
-
/**
|
|
9685
|
-
* Record kinds (D881): the declaration an addon makes of one kind of fact, the
|
|
9686
|
-
* Zod row schema that describes it, and the helper that turns the two into the
|
|
9687
|
-
* JSON the engine stores.
|
|
9688
|
-
*
|
|
9689
|
-
* A kind is declared ONCE, next to the code that owns it, and the engine's
|
|
9690
|
-
* reconciler creates its indexes - nothing creates DDL at runtime from a write.
|
|
9691
|
-
* Everything the generic machinery needs (secret fields, the entry descriptor)
|
|
9692
|
-
* is DERIVED from the Zod schema: `.meta({ secret: true })` marks a credential,
|
|
9693
|
-
* never a field name (D879).
|
|
9694
|
-
*/
|
|
9695
|
-
/** A JSON field path: `trackId`, `auth.accessToken`. Charset-restricted: it is rendered into DDL. */
|
|
9696
|
-
var RecordFieldPathSchema = string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,63}(\.[A-Za-z_][A-Za-z0-9_]{0,63}){0,3}$/);
|
|
9697
|
-
var RecordIndexSchema = object({
|
|
9698
|
-
name: string().regex(/^[a-z][a-z0-9_]{0,31}$/),
|
|
9699
|
-
fields: array(RecordFieldPathSchema).min(1).max(4),
|
|
9700
|
-
unique: boolean().optional()
|
|
9701
|
-
}).strict();
|
|
9702
|
-
var RecordRetentionSchema = union([
|
|
9703
|
-
object({
|
|
9704
|
-
ttlMs: number().int().positive(),
|
|
9705
|
-
from: _enum(["at", "write"])
|
|
9706
|
-
}).strict(),
|
|
9707
|
-
object({
|
|
9708
|
-
maxRows: number().int().positive().optional(),
|
|
9709
|
-
maxRowsPerDevice: number().int().positive().optional()
|
|
9710
|
-
}).strict().refine((r) => r.maxRows !== void 0 || r.maxRowsPerDevice !== void 0, { message: "name maxRows, maxRowsPerDevice or both" }),
|
|
9711
|
-
object({
|
|
9712
|
-
owner: string().min(1),
|
|
9713
|
-
reason: string().min(1)
|
|
9714
|
-
}).strict(),
|
|
9715
|
-
object({ none: string().min(1) }).strict()
|
|
9716
|
-
]);
|
|
9717
|
-
/** The descriptor a generic entries UI reads (D881 "generic entries"). */
|
|
9718
|
-
var EntryDescriptorSchema = object({
|
|
9719
|
-
title: string().min(1),
|
|
9720
|
-
/** The record body field shown as the entry's name. */
|
|
9721
|
-
nameField: string().min(1),
|
|
9722
|
-
/** JSON Schema of the entry body; `secret: true` marks a password field. */
|
|
9723
|
-
fields: record(string(), unknown())
|
|
9724
|
-
}).strict();
|
|
9725
|
-
/** What crosses the wire from an addon: JSON only, no Zod. An unknown key is refused. */
|
|
9726
|
-
var RecordKindDeclarationSchema = object({
|
|
9727
|
-
database: _enum(RECORD_DATABASES).default("settings"),
|
|
9728
|
-
class: _enum(RECORD_CLASSES),
|
|
9729
|
-
writeTrigger: string().min(1),
|
|
9730
|
-
device: _enum([
|
|
9731
|
-
"required",
|
|
9732
|
-
"optional",
|
|
9733
|
-
"none"
|
|
9734
|
-
]).default("none"),
|
|
9735
|
-
at: _enum(["required", "none"]).default("none"),
|
|
9736
|
-
indexes: array(RecordIndexSchema).max(8).default([]),
|
|
9737
|
-
retention: RecordRetentionSchema,
|
|
9738
|
-
deviceRemoval: _enum(["delete", "none"]).default("none"),
|
|
9739
|
-
maxDataBytes: number().int().positive().max(RECORD_MAX_DATA_BYTES).default(RECORD_DEFAULT_MAX_DATA_BYTES),
|
|
9740
|
-
/** Derived from the Zod schema; never written by hand. */
|
|
9741
|
-
secretPaths: array(string().min(1)).default([]),
|
|
9742
|
-
/** The id key of each array that carries a secret (`{ headers: 'name' }`). */
|
|
9743
|
-
secretArrayKeys: record(string(), string()).default({}),
|
|
9744
|
-
/** Kinds that must agree through `transact` sit in one file under one family name. */
|
|
9745
|
-
transactFamily: string().min(1).optional(),
|
|
9746
|
-
entry: EntryDescriptorSchema.optional()
|
|
9747
|
-
}).strict();
|
|
9748
|
-
/** The kind names: lower-case, dash separated, short enough for an index name. */
|
|
9749
|
-
var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
|
|
9750
|
-
/** Cross-field rules a declaration must satisfy. Throws {@link DeclarationRefusedError}. */
|
|
9751
|
-
function assertValidDeclaration(name, decl) {
|
|
9752
|
-
const refuse = (why) => {
|
|
9753
|
-
throw new DeclarationRefusedError(`kind "${name}": ${why}`);
|
|
9754
|
-
};
|
|
9755
|
-
if (!RecordKindNameSchema.safeParse(name).success) refuse("the name must be lower-case-dashed");
|
|
9756
|
-
const seen = /* @__PURE__ */ new Set();
|
|
9757
|
-
for (const index of decl.indexes) {
|
|
9758
|
-
if (seen.has(index.name)) refuse(`index name "${index.name}" is declared twice`);
|
|
9759
|
-
seen.add(index.name);
|
|
9760
|
-
}
|
|
9761
|
-
const { retention } = decl;
|
|
9762
|
-
const hasSecrets = decl.secretPaths.length > 0;
|
|
9763
|
-
if ("ttlMs" in retention) {
|
|
9764
|
-
if (retention.from === "at" && decl.at !== "required") refuse("a ttl from \"at\" needs at: \"required\"");
|
|
9765
|
-
if (hasSecrets) refuse("a kind with secret fields cannot use engine TTL (its keychain values would be orphaned)");
|
|
9766
|
-
}
|
|
9767
|
-
if ("maxRows" in retention || "maxRowsPerDevice" in retention) {
|
|
9768
|
-
if (retention.maxRowsPerDevice !== void 0 && decl.device !== "required") refuse("maxRowsPerDevice needs device: \"required\"");
|
|
9769
|
-
if (decl.at !== "required") refuse("a row bound trims the oldest \"at\": it needs at: \"required\"");
|
|
9770
|
-
if (hasSecrets) refuse("a kind with secret fields cannot use an engine row bound");
|
|
9771
|
-
const bound = Math.max(retention.maxRows ?? 0, retention.maxRowsPerDevice ?? 0);
|
|
9772
|
-
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)`);
|
|
9773
|
-
}
|
|
9774
|
-
if (decl.deviceRemoval === "delete" && decl.device === "none") refuse("deviceRemoval: \"delete\" needs a device: \"required\" or \"optional\"");
|
|
9775
|
-
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`);
|
|
9776
|
-
}
|
|
9777
|
-
function bodyFieldsOf(schema) {
|
|
9778
|
-
return { ...toJSONSchema(schema, {
|
|
9779
|
-
unrepresentable: "any",
|
|
9780
|
-
io: "input"
|
|
9781
|
-
}) };
|
|
9782
|
-
}
|
|
9783
|
-
function isRecordLike(value) {
|
|
9784
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9785
|
-
}
|
|
9786
|
-
/** Does every branch of the body schema carry `field`? (A discriminated union has several.) */
|
|
9787
|
-
function hasNameField(json, field) {
|
|
9788
|
-
const branches = json["oneOf"] ?? json["anyOf"];
|
|
9789
|
-
if (Array.isArray(branches)) return branches.length > 0 && branches.every((b) => isRecordLike(b) && hasNameField(b, field));
|
|
9790
|
-
const properties = json["properties"];
|
|
9791
|
-
return isRecordLike(properties) && Object.prototype.hasOwnProperty.call(properties, field);
|
|
9792
|
-
}
|
|
9793
|
-
/**
|
|
9794
|
-
* Declare the kinds of a package. The keys are the kind names. The result is
|
|
9795
|
-
* what `ctx.records.declare` sends to the engine and what `ctx.records.kind`
|
|
9796
|
-
* types its handle from.
|
|
9797
|
-
*/
|
|
9798
|
-
function defineRecordKinds(specs) {
|
|
9799
|
-
const out = {};
|
|
9800
|
-
for (const [name, spec] of Object.entries(specs)) {
|
|
9801
|
-
const secretPaths = collectZodSecretPaths(spec.schema);
|
|
9802
|
-
const nameField = spec.entry?.nameField ?? "name";
|
|
9803
|
-
const fields = bodyFieldsOf(spec.schema);
|
|
9804
|
-
if (spec.entry !== void 0 && !hasNameField(fields, nameField)) throw new DeclarationRefusedError(`kind "${name}": an entry kind's schema needs a "${nameField}" field`);
|
|
9805
|
-
const parsed = RecordKindDeclarationSchema.parse({
|
|
9806
|
-
database: spec.database,
|
|
9807
|
-
class: spec.class,
|
|
9808
|
-
writeTrigger: spec.writeTrigger,
|
|
9809
|
-
device: spec.device,
|
|
9810
|
-
at: spec.at,
|
|
9811
|
-
indexes: spec.indexes === void 0 ? void 0 : [...spec.indexes],
|
|
9812
|
-
retention: spec.retention,
|
|
9813
|
-
deviceRemoval: spec.deviceRemoval,
|
|
9814
|
-
maxDataBytes: spec.maxDataBytes,
|
|
9815
|
-
secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
|
|
9816
|
-
secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
|
|
9817
|
-
transactFamily: spec.transactFamily,
|
|
9818
|
-
entry: spec.entry === void 0 ? void 0 : {
|
|
9819
|
-
title: spec.entry.title,
|
|
9820
|
-
nameField,
|
|
9821
|
-
fields
|
|
9822
|
-
}
|
|
9823
|
-
});
|
|
9824
|
-
assertValidDeclaration(name, parsed);
|
|
9825
|
-
out[name] = {
|
|
9826
|
-
name,
|
|
9827
|
-
schema: spec.schema,
|
|
9828
|
-
declaration: parsed
|
|
9829
|
-
};
|
|
9830
|
-
}
|
|
9831
|
-
return out;
|
|
9832
|
-
}
|
|
9833
|
-
/**
|
|
9834
|
-
* Entry kinds (D881): every list of user-created entries (notification
|
|
9835
|
-
* targets, brokers, LLM profiles) is ONE record kind with an id, a name and a
|
|
9836
|
-
* kind-specific body, and the admin UI lists/adds/edits/deletes all of them
|
|
9837
|
-
* through the `record-entries` capability with password fields derived from
|
|
9838
|
-
* the schema (`.meta({ secret: true })`).
|
|
9839
|
-
*/
|
|
9840
|
-
/** The body of a plain entry: a name and the fields the kind adds. */
|
|
9841
|
-
function entryBody(shape) {
|
|
9842
|
-
return object({
|
|
9843
|
-
name: string().min(1),
|
|
9844
|
-
enabled: boolean().optional(),
|
|
9845
|
-
...shape
|
|
9846
|
-
});
|
|
9847
|
-
}
|
|
9848
|
-
/**
|
|
9849
|
-
* Declare one entry kind. An entry is configuration: never trimmed by the
|
|
9850
|
-
* engine, never aged, belonging to no device.
|
|
9851
|
-
*/
|
|
9852
|
-
function defineEntryKind(name, spec) {
|
|
9853
|
-
const defined = defineRecordKinds({ [name]: {
|
|
9854
|
-
schema: spec.schema,
|
|
9855
|
-
class: "config",
|
|
9856
|
-
writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
|
|
9857
|
-
retention: { none: "an entry goes only when the operator deletes it" },
|
|
9858
|
-
...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
|
|
9859
|
-
...spec.database === void 0 ? {} : { database: spec.database },
|
|
9860
|
-
...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
|
|
9861
|
-
...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
|
|
9862
|
-
...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
|
|
9863
|
-
entry: {
|
|
9864
|
-
title: spec.title,
|
|
9865
|
-
...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
|
|
9866
|
-
}
|
|
9867
|
-
} })[name];
|
|
9868
|
-
if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
|
|
9869
|
-
return defined;
|
|
9870
|
-
}
|
|
9871
9618
|
object({
|
|
9872
9619
|
field: string().min(1),
|
|
9873
9620
|
on: literal("advance")
|
|
@@ -9960,6 +9707,286 @@ function optionalMethod(input, output, options) {
|
|
|
9960
9707
|
function event(data) {
|
|
9961
9708
|
return { data };
|
|
9962
9709
|
}
|
|
9710
|
+
function defOf(schema) {
|
|
9711
|
+
const zodBag = Reflect.get(schema, "_zod");
|
|
9712
|
+
if (typeof zodBag !== "object" || zodBag === null) return {};
|
|
9713
|
+
const def = Reflect.get(zodBag, "def");
|
|
9714
|
+
if (typeof def !== "object" || def === null) return {};
|
|
9715
|
+
return def;
|
|
9716
|
+
}
|
|
9717
|
+
function declaredHere(schema) {
|
|
9718
|
+
return globalRegistry.get(schema)?.secret === true;
|
|
9719
|
+
}
|
|
9720
|
+
/** Wrapper kinds whose `innerType` is the field itself. */
|
|
9721
|
+
var WRAPPERS = new Set([
|
|
9722
|
+
"optional",
|
|
9723
|
+
"nullable",
|
|
9724
|
+
"default",
|
|
9725
|
+
"prefault",
|
|
9726
|
+
"catch",
|
|
9727
|
+
"readonly",
|
|
9728
|
+
"nonoptional"
|
|
9729
|
+
]);
|
|
9730
|
+
/**
|
|
9731
|
+
* The schema layers that make up ONE field: the schema itself and every
|
|
9732
|
+
* wrapper below it (optional / default / nullable / readonly / catch), plus
|
|
9733
|
+
* both sides of a pipe (`z.preprocess` puts the real schema on `out`,
|
|
9734
|
+
* `.transform` on `in`).
|
|
9735
|
+
*/
|
|
9736
|
+
function layersOf(schema) {
|
|
9737
|
+
const out = [];
|
|
9738
|
+
const queue = [schema];
|
|
9739
|
+
while (queue.length > 0) {
|
|
9740
|
+
const s = queue.shift();
|
|
9741
|
+
if (!s || out.includes(s)) continue;
|
|
9742
|
+
out.push(s);
|
|
9743
|
+
const def = defOf(s);
|
|
9744
|
+
if (def.type !== void 0 && WRAPPERS.has(def.type) && def.innerType) queue.push(def.innerType);
|
|
9745
|
+
if (def.type === "pipe") {
|
|
9746
|
+
if (def.in) queue.push(def.in);
|
|
9747
|
+
if (def.out) queue.push(def.out);
|
|
9748
|
+
}
|
|
9749
|
+
}
|
|
9750
|
+
return out;
|
|
9751
|
+
}
|
|
9752
|
+
/** True when any layer of this field declares `.meta({ secret: true })`. */
|
|
9753
|
+
function isZodSecret(schema) {
|
|
9754
|
+
return layersOf(schema).some(declaredHere);
|
|
9755
|
+
}
|
|
9756
|
+
/**
|
|
9757
|
+
* Every declared secret path in a config schema: dotted keys, `[]` for "each
|
|
9758
|
+
* element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
|
|
9759
|
+
* map" (`applianceSecrets{}`). A declared
|
|
9760
|
+
* object or array is reported as one path and not descended into — everything
|
|
9761
|
+
* under it is secret.
|
|
9762
|
+
*/
|
|
9763
|
+
function collectZodSecretPaths(schema, prefix = "") {
|
|
9764
|
+
if (isZodSecret(schema)) return prefix.length > 0 ? [prefix] : [];
|
|
9765
|
+
const paths = [];
|
|
9766
|
+
for (const layer of layersOf(schema)) {
|
|
9767
|
+
const def = defOf(layer);
|
|
9768
|
+
if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
|
|
9769
|
+
else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
|
|
9770
|
+
else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
|
|
9771
|
+
else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
|
|
9772
|
+
else if (def.type === "intersection") {
|
|
9773
|
+
if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
|
|
9774
|
+
if (def.right) paths.push(...collectZodSecretPaths(def.right, prefix));
|
|
9775
|
+
}
|
|
9776
|
+
}
|
|
9777
|
+
return [...new Set(paths)];
|
|
9778
|
+
}
|
|
9779
|
+
/**
|
|
9780
|
+
* Record kinds (D881): the declaration an addon makes of one kind of fact, the
|
|
9781
|
+
* Zod row schema that describes it, and the helper that turns the two into the
|
|
9782
|
+
* JSON the engine stores.
|
|
9783
|
+
*
|
|
9784
|
+
* A kind is declared ONCE, next to the code that owns it, and the engine's
|
|
9785
|
+
* reconciler creates its indexes - nothing creates DDL at runtime from a write.
|
|
9786
|
+
* Everything the generic machinery needs (secret fields, the entry descriptor)
|
|
9787
|
+
* is DERIVED from the Zod schema: `.meta({ secret: true })` marks a credential,
|
|
9788
|
+
* never a field name (D879).
|
|
9789
|
+
*/
|
|
9790
|
+
/** A JSON field path: `trackId`, `auth.accessToken`. Charset-restricted: it is rendered into DDL. */
|
|
9791
|
+
var RecordFieldPathSchema = string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,63}(\.[A-Za-z_][A-Za-z0-9_]{0,63}){0,3}$/);
|
|
9792
|
+
var RecordIndexSchema = object({
|
|
9793
|
+
name: string().regex(/^[a-z][a-z0-9_]{0,31}$/),
|
|
9794
|
+
fields: array(RecordFieldPathSchema).min(1).max(4),
|
|
9795
|
+
unique: boolean().optional()
|
|
9796
|
+
}).strict();
|
|
9797
|
+
var RecordRetentionSchema = union([
|
|
9798
|
+
object({
|
|
9799
|
+
ttlMs: number().int().positive(),
|
|
9800
|
+
from: _enum(["at", "write"])
|
|
9801
|
+
}).strict(),
|
|
9802
|
+
object({
|
|
9803
|
+
maxRows: number().int().positive().optional(),
|
|
9804
|
+
maxRowsPerDevice: number().int().positive().optional()
|
|
9805
|
+
}).strict().refine((r) => r.maxRows !== void 0 || r.maxRowsPerDevice !== void 0, { message: "name maxRows, maxRowsPerDevice or both" }),
|
|
9806
|
+
object({
|
|
9807
|
+
owner: string().min(1),
|
|
9808
|
+
reason: string().min(1)
|
|
9809
|
+
}).strict(),
|
|
9810
|
+
object({ none: string().min(1) }).strict()
|
|
9811
|
+
]);
|
|
9812
|
+
/** The descriptor a generic entries UI reads (D881 "generic entries"). */
|
|
9813
|
+
var EntryDescriptorSchema = object({
|
|
9814
|
+
title: string().min(1),
|
|
9815
|
+
/** The record body field shown as the entry's name. */
|
|
9816
|
+
nameField: string().min(1),
|
|
9817
|
+
/** JSON Schema of the entry body; `secret: true` marks a password field. */
|
|
9818
|
+
fields: record(string(), unknown())
|
|
9819
|
+
}).strict();
|
|
9820
|
+
/** What crosses the wire from an addon: JSON only, no Zod. An unknown key is refused. */
|
|
9821
|
+
var RecordKindDeclarationSchema = object({
|
|
9822
|
+
database: _enum(RECORD_DATABASES).default("settings"),
|
|
9823
|
+
class: _enum(RECORD_CLASSES),
|
|
9824
|
+
writeTrigger: string().min(1),
|
|
9825
|
+
device: _enum([
|
|
9826
|
+
"required",
|
|
9827
|
+
"optional",
|
|
9828
|
+
"none"
|
|
9829
|
+
]).default("none"),
|
|
9830
|
+
at: _enum(["required", "none"]).default("none"),
|
|
9831
|
+
indexes: array(RecordIndexSchema).max(8).default([]),
|
|
9832
|
+
retention: RecordRetentionSchema,
|
|
9833
|
+
deviceRemoval: _enum(["delete", "none"]).default("none"),
|
|
9834
|
+
maxDataBytes: number().int().positive().max(RECORD_MAX_DATA_BYTES).default(RECORD_DEFAULT_MAX_DATA_BYTES),
|
|
9835
|
+
/** Derived from the Zod schema; never written by hand. */
|
|
9836
|
+
secretPaths: array(string().min(1)).default([]),
|
|
9837
|
+
/** The id key of each array that carries a secret (`{ headers: 'name' }`). */
|
|
9838
|
+
secretArrayKeys: record(string(), string()).default({}),
|
|
9839
|
+
/** Kinds that must agree through `transact` sit in one file under one family name. */
|
|
9840
|
+
transactFamily: string().min(1).optional(),
|
|
9841
|
+
entry: EntryDescriptorSchema.optional()
|
|
9842
|
+
}).strict();
|
|
9843
|
+
/** The kind names: lower-case, dash separated, short enough for an index name. */
|
|
9844
|
+
var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
|
|
9845
|
+
/** Cross-field rules a declaration must satisfy. Throws {@link DeclarationRefusedError}. */
|
|
9846
|
+
function assertValidDeclaration(name, decl) {
|
|
9847
|
+
const refuse = (why) => {
|
|
9848
|
+
throw new DeclarationRefusedError(`kind "${name}": ${why}`);
|
|
9849
|
+
};
|
|
9850
|
+
if (!RecordKindNameSchema.safeParse(name).success) refuse("the name must be lower-case-dashed");
|
|
9851
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9852
|
+
for (const index of decl.indexes) {
|
|
9853
|
+
if (seen.has(index.name)) refuse(`index name "${index.name}" is declared twice`);
|
|
9854
|
+
seen.add(index.name);
|
|
9855
|
+
}
|
|
9856
|
+
const { retention } = decl;
|
|
9857
|
+
const hasSecrets = decl.secretPaths.length > 0;
|
|
9858
|
+
if ("ttlMs" in retention) {
|
|
9859
|
+
if (retention.from === "at" && decl.at !== "required") refuse("a ttl from \"at\" needs at: \"required\"");
|
|
9860
|
+
if (hasSecrets) refuse("a kind with secret fields cannot use engine TTL (its keychain values would be orphaned)");
|
|
9861
|
+
}
|
|
9862
|
+
if ("maxRows" in retention || "maxRowsPerDevice" in retention) {
|
|
9863
|
+
if (retention.maxRowsPerDevice !== void 0 && decl.device !== "required") refuse("maxRowsPerDevice needs device: \"required\"");
|
|
9864
|
+
if (decl.at !== "required") refuse("a row bound trims the oldest \"at\": it needs at: \"required\"");
|
|
9865
|
+
if (hasSecrets) refuse("a kind with secret fields cannot use an engine row bound");
|
|
9866
|
+
const bound = Math.max(retention.maxRows ?? 0, retention.maxRowsPerDevice ?? 0);
|
|
9867
|
+
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)`);
|
|
9868
|
+
}
|
|
9869
|
+
if (decl.deviceRemoval === "delete" && decl.device === "none") refuse("deviceRemoval: \"delete\" needs a device: \"required\" or \"optional\"");
|
|
9870
|
+
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`);
|
|
9871
|
+
}
|
|
9872
|
+
function bodyFieldsOf(schema) {
|
|
9873
|
+
return { ...toJSONSchema(schema, {
|
|
9874
|
+
unrepresentable: "any",
|
|
9875
|
+
io: "input"
|
|
9876
|
+
}) };
|
|
9877
|
+
}
|
|
9878
|
+
function isRecordLike(value) {
|
|
9879
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9880
|
+
}
|
|
9881
|
+
/** Does every branch of the body schema carry `field`? (A discriminated union has several.) */
|
|
9882
|
+
function hasNameField(json, field) {
|
|
9883
|
+
const branches = json["oneOf"] ?? json["anyOf"];
|
|
9884
|
+
if (Array.isArray(branches)) return branches.length > 0 && branches.every((b) => isRecordLike(b) && hasNameField(b, field));
|
|
9885
|
+
const properties = json["properties"];
|
|
9886
|
+
return isRecordLike(properties) && Object.prototype.hasOwnProperty.call(properties, field);
|
|
9887
|
+
}
|
|
9888
|
+
/**
|
|
9889
|
+
* Declare the kinds of a package. The keys are the kind names. The result is
|
|
9890
|
+
* what `ctx.records.declare` sends to the engine and what `ctx.records.kind`
|
|
9891
|
+
* types its handle from.
|
|
9892
|
+
*/
|
|
9893
|
+
function defineRecordKinds(specs) {
|
|
9894
|
+
const out = {};
|
|
9895
|
+
for (const [name, spec] of Object.entries(specs)) {
|
|
9896
|
+
const secretPaths = collectZodSecretPaths(spec.schema);
|
|
9897
|
+
const nameField = spec.entry?.nameField ?? "name";
|
|
9898
|
+
const fields = bodyFieldsOf(spec.schema);
|
|
9899
|
+
if (spec.entry !== void 0 && !hasNameField(fields, nameField)) throw new DeclarationRefusedError(`kind "${name}": an entry kind's schema needs a "${nameField}" field`);
|
|
9900
|
+
const parsed = RecordKindDeclarationSchema.parse({
|
|
9901
|
+
database: spec.database,
|
|
9902
|
+
class: spec.class,
|
|
9903
|
+
writeTrigger: spec.writeTrigger,
|
|
9904
|
+
device: spec.device,
|
|
9905
|
+
at: spec.at,
|
|
9906
|
+
indexes: spec.indexes === void 0 ? void 0 : [...spec.indexes],
|
|
9907
|
+
retention: spec.retention,
|
|
9908
|
+
deviceRemoval: spec.deviceRemoval,
|
|
9909
|
+
maxDataBytes: spec.maxDataBytes,
|
|
9910
|
+
secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
|
|
9911
|
+
secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
|
|
9912
|
+
transactFamily: spec.transactFamily,
|
|
9913
|
+
entry: spec.entry === void 0 ? void 0 : {
|
|
9914
|
+
title: spec.entry.title,
|
|
9915
|
+
nameField,
|
|
9916
|
+
fields
|
|
9917
|
+
}
|
|
9918
|
+
});
|
|
9919
|
+
assertValidDeclaration(name, parsed);
|
|
9920
|
+
out[name] = {
|
|
9921
|
+
name,
|
|
9922
|
+
schema: spec.schema,
|
|
9923
|
+
declaration: parsed
|
|
9924
|
+
};
|
|
9925
|
+
}
|
|
9926
|
+
return out;
|
|
9927
|
+
}
|
|
9928
|
+
/**
|
|
9929
|
+
* Entry kinds (D881): every list of user-created entries (notification
|
|
9930
|
+
* targets, brokers, LLM profiles) is ONE record kind with an id, a name and a
|
|
9931
|
+
* kind-specific body, and the admin UI lists/adds/edits/deletes all of them
|
|
9932
|
+
* through the `record-entries` capability with password fields derived from
|
|
9933
|
+
* the schema (`.meta({ secret: true })`).
|
|
9934
|
+
*/
|
|
9935
|
+
/** The body of a plain entry: a name and the fields the kind adds. */
|
|
9936
|
+
function entryBody(shape) {
|
|
9937
|
+
return object({
|
|
9938
|
+
name: string().min(1),
|
|
9939
|
+
enabled: boolean().optional(),
|
|
9940
|
+
...shape
|
|
9941
|
+
});
|
|
9942
|
+
}
|
|
9943
|
+
/**
|
|
9944
|
+
* Declare one entry kind. An entry is configuration: never trimmed by the
|
|
9945
|
+
* engine, never aged, belonging to no device.
|
|
9946
|
+
*/
|
|
9947
|
+
function defineEntryKind(name, spec) {
|
|
9948
|
+
const defined = defineRecordKinds({ [name]: {
|
|
9949
|
+
schema: spec.schema,
|
|
9950
|
+
class: "config",
|
|
9951
|
+
writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
|
|
9952
|
+
retention: { none: "an entry goes only when the operator deletes it" },
|
|
9953
|
+
...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
|
|
9954
|
+
...spec.database === void 0 ? {} : { database: spec.database },
|
|
9955
|
+
...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
|
|
9956
|
+
...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
|
|
9957
|
+
...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
|
|
9958
|
+
entry: {
|
|
9959
|
+
title: spec.title,
|
|
9960
|
+
...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
|
|
9961
|
+
}
|
|
9962
|
+
} })[name];
|
|
9963
|
+
if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
|
|
9964
|
+
return defined;
|
|
9965
|
+
}
|
|
9966
|
+
/**
|
|
9967
|
+
* The share-view scope kinds a share-link token can carry. The hub mints
|
|
9968
|
+
* and validates them (`ShareViewGrantSchema` builds its
|
|
9969
|
+
* enum from this list); a data plane names the kinds it ACCEPTS in `share`.
|
|
9970
|
+
*/
|
|
9971
|
+
var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
|
|
9972
|
+
/** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
|
|
9973
|
+
var PeerBytesTicketSchema = object({
|
|
9974
|
+
/** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
|
|
9975
|
+
url: string().min(1),
|
|
9976
|
+
/**
|
|
9977
|
+
* The HOST node this URL means something on — the hub or a named agent,
|
|
9978
|
+
* never a runner. {@link AddonPeerBytes.open} compares it to its own and
|
|
9979
|
+
* refuses `cross-node` by name when they differ, without dialling.
|
|
9980
|
+
*/
|
|
9981
|
+
hostNodeId: string().min(1),
|
|
9982
|
+
expiresAtMs: number().int().nonnegative(),
|
|
9983
|
+
/**
|
|
9984
|
+
* What the producer DECLARED the body to be, when it knows — `null` when it
|
|
9985
|
+
* does not. Never `0` for unknown (D393): a consumer sizing a bound off this
|
|
9986
|
+
* must be able to tell "the producer did not say" from "the body is empty".
|
|
9987
|
+
*/
|
|
9988
|
+
declaredBytes: number().int().nonnegative().nullable()
|
|
9989
|
+
});
|
|
9963
9990
|
var READING_QUANTITIES = [
|
|
9964
9991
|
"temperature",
|
|
9965
9992
|
"pressure",
|
|
@@ -10538,7 +10565,6 @@ var CAP_NAMES = Object.freeze([
|
|
|
10538
10565
|
"climate-control",
|
|
10539
10566
|
"color",
|
|
10540
10567
|
"composer",
|
|
10541
|
-
"connection-test",
|
|
10542
10568
|
"connectivity",
|
|
10543
10569
|
"consumables",
|
|
10544
10570
|
"contact",
|
|
@@ -12558,8 +12584,7 @@ Object.freeze({
|
|
|
12558
12584
|
"offerArtifact": 3e5,
|
|
12559
12585
|
"readResult": 3e5,
|
|
12560
12586
|
"submitJob": 18e5,
|
|
12561
|
-
"uploadDataset": 72e5
|
|
12562
|
-
"validateCredentials": 3e4
|
|
12587
|
+
"uploadDataset": 72e5
|
|
12563
12588
|
}
|
|
12564
12589
|
});
|
|
12565
12590
|
object({
|
|
@@ -13914,6 +13939,432 @@ var IntegrationInstanceRefSchema = object({
|
|
|
13914
13939
|
]),
|
|
13915
13940
|
candidateStableId: string().optional()
|
|
13916
13941
|
});
|
|
13942
|
+
/**
|
|
13943
|
+
* notification-output — canonical, capability-gated notification delivery.
|
|
13944
|
+
*
|
|
13945
|
+
* Apprise-derived model (see
|
|
13946
|
+
* `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
|
|
13947
|
+
* callers emit ONE canonical `Notification`; each provider declares a
|
|
13948
|
+
* per-kind capability descriptor (`TargetKind`), and the pure degrade
|
|
13949
|
+
* engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
|
|
13950
|
+
* message to what the kind supports — callers never special-case a service.
|
|
13951
|
+
*
|
|
13952
|
+
* DESIGN DECISIONS (locked):
|
|
13953
|
+
* - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
|
|
13954
|
+
* `setTargetEnabled`), each provider persisting via the `settings-store`
|
|
13955
|
+
* cap. Rationale: the admin UI needs one uniform surface across the
|
|
13956
|
+
* notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
|
|
13957
|
+
* alternative would fork the UI per addon and cannot host the
|
|
13958
|
+
* discovery→adopt flow.
|
|
13959
|
+
* - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
|
|
13960
|
+
* the generated cap-mount auto-`concatCollection`-fans them across every
|
|
13961
|
+
* registered provider (notifiers addon + HA addon) so one catalog is
|
|
13962
|
+
* routable. `send` / `testTarget` / CRUD route to ONE provider by the
|
|
13963
|
+
* `addonId` the generated collection router extracts from the call input.
|
|
13964
|
+
* - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
|
|
13965
|
+
* transformer) + UDS MsgPack both round-trip typed arrays — already used by
|
|
13966
|
+
* `storage` / `storage-provider` / `recording` caps over the same path. No
|
|
13967
|
+
* base64 fallback needed.
|
|
13968
|
+
*
|
|
13969
|
+
* TODO (deferred, closed-set change — separate decision): add
|
|
13970
|
+
* `providerKind: 'notify'` so notification providers surface on the unified
|
|
13971
|
+
* admin "Integrations" page.
|
|
13972
|
+
*/
|
|
13973
|
+
/**
|
|
13974
|
+
* Zentik-derived typed-media enum — the superset across every kind. Each
|
|
13975
|
+
* adapter picks what it supports and the degrade engine filters the rest.
|
|
13976
|
+
*/
|
|
13977
|
+
var AttachmentMediaTypeSchema = _enum([
|
|
13978
|
+
"image",
|
|
13979
|
+
"video",
|
|
13980
|
+
"gif",
|
|
13981
|
+
"audio",
|
|
13982
|
+
"icon"
|
|
13983
|
+
]);
|
|
13984
|
+
/**
|
|
13985
|
+
* A single attachment. Exactly one of `url` (remote source, most adapters
|
|
13986
|
+
* prefer this) or `bytes` (inline source; required for Pushover-style
|
|
13987
|
+
* bytes-only kinds) MUST be present — the degrade engine expresses a
|
|
13988
|
+
* url→bytes fetch as a `needsFetch` directive the adapter executes.
|
|
13989
|
+
*/
|
|
13990
|
+
var AttachmentSchema = object({
|
|
13991
|
+
mediaType: AttachmentMediaTypeSchema,
|
|
13992
|
+
url: string().optional(),
|
|
13993
|
+
bytes: _instanceof(Uint8Array).optional(),
|
|
13994
|
+
mime: string().optional(),
|
|
13995
|
+
name: string().optional()
|
|
13996
|
+
}).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
|
|
13997
|
+
var NotificationFormatSchema = _enum([
|
|
13998
|
+
"text",
|
|
13999
|
+
"markdown",
|
|
14000
|
+
"html"
|
|
14001
|
+
]);
|
|
14002
|
+
/**
|
|
14003
|
+
* The CLOSED icon vocabulary an action button may use.
|
|
14004
|
+
*
|
|
14005
|
+
* A closed set, not a free string, and that is the whole point: an arbitrary
|
|
14006
|
+
* icon name is one that ntfy renders, zentik silently drops, and nobody
|
|
14007
|
+
* notices — the same class of gap as a zone vocabulary nothing produced
|
|
14008
|
+
* ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
|
|
14009
|
+
* declares `actionIcons: false` and the degrade engine strips the field.
|
|
14010
|
+
*
|
|
14011
|
+
* Named by INTENT, never by glyph. "check" would tie the vocabulary to one
|
|
14012
|
+
* renderer's icon set; "acknowledge" survives an adapter that draws it
|
|
14013
|
+
* differently.
|
|
14014
|
+
*
|
|
14015
|
+
* ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
|
|
14016
|
+
*
|
|
14017
|
+
* These names are for US. **No adapter may forward one verbatim.** Each maps
|
|
14018
|
+
* the whole set onto its own renderer's vocabulary through a
|
|
14019
|
+
* `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
|
|
14020
|
+
* fallback, so adding a member here fails every adapter's build until someone
|
|
14021
|
+
* decides its glyph, which is the only place that decision can be made
|
|
14022
|
+
* honestly.
|
|
14023
|
+
*
|
|
14024
|
+
* This paragraph is the bug. Zentik declared `actionIcons: true` and passed
|
|
14025
|
+
* `disarm` straight through; iOS feeds that string to
|
|
14026
|
+
* `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
|
|
14027
|
+
* and every snooze and alarm button arrived BLANK. A pass-through is not a
|
|
14028
|
+
* mapping, and "the field is documented" is not "the value renders".
|
|
14029
|
+
*
|
|
14030
|
+
* Adding a member is TRAIN-BOUND. The enum lives in the published
|
|
14031
|
+
* `@camstack/server` closure and the cap seam validates against the HUB's copy,
|
|
14032
|
+
* so an addon that emits a token the running hub does not know does not lose an
|
|
14033
|
+
* icon — its whole `send` fails Zod validation and the notification never
|
|
14034
|
+
* arrives. Never emit a new token from an addon before the train carrying it.
|
|
14035
|
+
*/
|
|
14036
|
+
var NotificationActionIconSchema = _enum([
|
|
14037
|
+
"acknowledge",
|
|
14038
|
+
"dismiss",
|
|
14039
|
+
"silence",
|
|
14040
|
+
"snooze",
|
|
14041
|
+
"view",
|
|
14042
|
+
"play",
|
|
14043
|
+
"open",
|
|
14044
|
+
"close",
|
|
14045
|
+
"lock",
|
|
14046
|
+
"unlock",
|
|
14047
|
+
"arm",
|
|
14048
|
+
"arm-home",
|
|
14049
|
+
"arm-away",
|
|
14050
|
+
"arm-night",
|
|
14051
|
+
"disarm",
|
|
14052
|
+
"light",
|
|
14053
|
+
"alert",
|
|
14054
|
+
"camera"
|
|
14055
|
+
]);
|
|
14056
|
+
/** A single tap-through action button. */
|
|
14057
|
+
var NotificationActionSchema = object({
|
|
14058
|
+
id: string(),
|
|
14059
|
+
label: string(),
|
|
14060
|
+
url: string().optional(),
|
|
14061
|
+
/** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
|
|
14062
|
+
icon: NotificationActionIconSchema.optional(),
|
|
14063
|
+
/**
|
|
14064
|
+
* Renders in a warning style where the notifier supports it.
|
|
14065
|
+
*
|
|
14066
|
+
* A HINT, never a gate. The callback's authority is its token and nothing
|
|
14067
|
+
* else — see `notification-center/action-token.ts` for what that does and
|
|
14068
|
+
* does not buy.
|
|
14069
|
+
*/
|
|
14070
|
+
destructive: boolean().optional(),
|
|
14071
|
+
/**
|
|
14072
|
+
* How the tap should REACH the url.
|
|
14073
|
+
*
|
|
14074
|
+
* `navigate` (absent, and every button authored before this field) opens it:
|
|
14075
|
+
* the phone leaves the notification and shows whatever the callback returns.
|
|
14076
|
+
* That is right for a button whose answer the operator wants to read.
|
|
14077
|
+
*
|
|
14078
|
+
* `background` fires it as a POST and stays put. It exists for the buttons
|
|
14079
|
+
* whose whole point is not to interrupt — "silence this for 30 minutes" is
|
|
14080
|
+
* an answer to the notification, and being thrown into a browser tab to
|
|
14081
|
+
* confirm it costs more attention than the notification did. A backend that
|
|
14082
|
+
* cannot do a background call renders it as an ordinary link (the adapters
|
|
14083
|
+
* fall back rather than dropping the button), so this is a preference, never
|
|
14084
|
+
* a requirement.
|
|
14085
|
+
*/
|
|
14086
|
+
mode: _enum(["navigate", "background"]).optional()
|
|
14087
|
+
});
|
|
14088
|
+
/** A delivery parameter value: what a notifier declared it takes. */
|
|
14089
|
+
var DeliveryParamValueSchema = union([
|
|
14090
|
+
string(),
|
|
14091
|
+
number(),
|
|
14092
|
+
boolean()
|
|
14093
|
+
]);
|
|
14094
|
+
/**
|
|
14095
|
+
* One extra delivery parameter a notifier KIND declares (D938). A rule that
|
|
14096
|
+
* addresses a target of that kind may set it; a kind that declares nothing is
|
|
14097
|
+
* never handed one. The admin UI derives its control from this descriptor —
|
|
14098
|
+
* no editor knows a service by name.
|
|
14099
|
+
*
|
|
14100
|
+
* Optional fields are `.optional()`, never `.default()` (a Zod default does not
|
|
14101
|
+
* run on the addon cap path).
|
|
14102
|
+
*/
|
|
14103
|
+
var DeliveryParamSchema = object({
|
|
14104
|
+
key: string().min(1),
|
|
14105
|
+
label: string(),
|
|
14106
|
+
/** i18n key of the label, resolved by the declaring addon's namespace. */
|
|
14107
|
+
labelKey: string().optional(),
|
|
14108
|
+
type: _enum([
|
|
14109
|
+
"enum",
|
|
14110
|
+
"number",
|
|
14111
|
+
"boolean",
|
|
14112
|
+
"string"
|
|
14113
|
+
]),
|
|
14114
|
+
/** `enum` only: the closed set of accepted values. */
|
|
14115
|
+
options: array(object({
|
|
14116
|
+
value: string(),
|
|
14117
|
+
label: string()
|
|
14118
|
+
})).optional(),
|
|
14119
|
+
/** `number` only. */
|
|
14120
|
+
min: number().optional(),
|
|
14121
|
+
max: number().optional(),
|
|
14122
|
+
/** What the notifier does when the rule sets nothing; shown as the placeholder. */
|
|
14123
|
+
default: DeliveryParamValueSchema.optional()
|
|
14124
|
+
});
|
|
14125
|
+
/**
|
|
14126
|
+
* The canonical notification. `body` is the only hard field (Apprise model).
|
|
14127
|
+
* `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
|
|
14128
|
+
* NOT a fixed severity enum — each kind declares its own `caps.levels` and
|
|
14129
|
+
* the adapter maps this ordinal onto its native level. `level?` is an
|
|
14130
|
+
* optional kind-native level id (`emergency`, `silent`, …) that overrides
|
|
14131
|
+
* `priority` for that one target.
|
|
14132
|
+
*/
|
|
14133
|
+
var NotificationSchema = object({
|
|
14134
|
+
body: string(),
|
|
14135
|
+
title: string().optional(),
|
|
14136
|
+
format: NotificationFormatSchema.default("text"),
|
|
14137
|
+
priority: number().int().min(1).max(5).default(3),
|
|
14138
|
+
level: string().optional(),
|
|
14139
|
+
attachments: array(AttachmentSchema).optional(),
|
|
14140
|
+
clickUrl: string().optional(),
|
|
14141
|
+
actions: array(NotificationActionSchema).optional(),
|
|
14142
|
+
sound: string().optional(),
|
|
14143
|
+
ttl: number().optional(),
|
|
14144
|
+
tag: string().optional(),
|
|
14145
|
+
deviceId: number().optional(),
|
|
14146
|
+
eventId: string().optional(),
|
|
14147
|
+
metadata: record(string(), unknown()).optional(),
|
|
14148
|
+
/**
|
|
14149
|
+
* Values for the parameters the target's kind DECLARES in
|
|
14150
|
+
* {@link TargetKindSchema} `deliveryParams`. Already validated against the
|
|
14151
|
+
* declaration by the sender; a notifier reads only the keys it declared.
|
|
14152
|
+
*/
|
|
14153
|
+
delivery: record(string(), DeliveryParamValueSchema).optional()
|
|
14154
|
+
});
|
|
14155
|
+
/** One declared native severity/priority level for a kind. */
|
|
14156
|
+
var TargetKindLevelSchema = object({
|
|
14157
|
+
id: string(),
|
|
14158
|
+
label: string(),
|
|
14159
|
+
/** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
|
|
14160
|
+
ordinal: number().int().min(1).max(5).nullable(),
|
|
14161
|
+
flags: object({
|
|
14162
|
+
critical: boolean().optional(),
|
|
14163
|
+
silent: boolean().optional(),
|
|
14164
|
+
noPush: boolean().optional()
|
|
14165
|
+
}).optional(),
|
|
14166
|
+
/** e.g. Pushover `emergency` requires `retry` / `expire`. */
|
|
14167
|
+
requires: array(string()).optional(),
|
|
14168
|
+
description: string().optional()
|
|
14169
|
+
});
|
|
14170
|
+
/** The full capability block consulted before dispatch. */
|
|
14171
|
+
var TargetKindCapsSchema = object({
|
|
14172
|
+
attachments: object({
|
|
14173
|
+
mediaTypes: array(AttachmentMediaTypeSchema),
|
|
14174
|
+
mode: _enum([
|
|
14175
|
+
"url",
|
|
14176
|
+
"bytes",
|
|
14177
|
+
"both"
|
|
14178
|
+
]),
|
|
14179
|
+
max: number().int().nonnegative(),
|
|
14180
|
+
maxBytes: number().int().positive().optional()
|
|
14181
|
+
}),
|
|
14182
|
+
/** Max action buttons (0 = none). */
|
|
14183
|
+
actions: number().int().nonnegative(),
|
|
14184
|
+
/**
|
|
14185
|
+
* Whether this kind renders a per-action ICON.
|
|
14186
|
+
*
|
|
14187
|
+
* `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
|
|
14188
|
+
* run on the addon cap path — three production failures in one day taught
|
|
14189
|
+
* this repo that once. Absent is read as false by the degrade engine, which
|
|
14190
|
+
* is the safe direction: an icon that is not rendered costs nothing, an icon
|
|
14191
|
+
* assumed and dropped costs the operator's trust in the field.
|
|
14192
|
+
*/
|
|
14193
|
+
actionIcons: boolean().optional(),
|
|
14194
|
+
levels: array(TargetKindLevelSchema),
|
|
14195
|
+
format: array(NotificationFormatSchema),
|
|
14196
|
+
clickUrl: boolean(),
|
|
14197
|
+
sound: boolean(),
|
|
14198
|
+
ttl: boolean(),
|
|
14199
|
+
bodyMaxLen: number().int().positive()
|
|
14200
|
+
});
|
|
14201
|
+
/**
|
|
14202
|
+
* `configSchema` is a `ConfigUISchema` tree passed through to the admin
|
|
14203
|
+
* FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
|
|
14204
|
+
* `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
|
|
14205
|
+
* the union is large and not meant for runtime validation here; the exported
|
|
14206
|
+
* `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
|
|
14207
|
+
*/
|
|
14208
|
+
var ConfigSchemaPassthrough$4 = unknown();
|
|
14209
|
+
var TargetKindSchema = object({
|
|
14210
|
+
kind: string(),
|
|
14211
|
+
label: string(),
|
|
14212
|
+
icon: string(),
|
|
14213
|
+
/** Stamped by each provider so the concat-fanned catalog stays routable. */
|
|
14214
|
+
addonId: string(),
|
|
14215
|
+
/**
|
|
14216
|
+
* URL of the kind's bundled BRAND icon, served by the providing addon over
|
|
14217
|
+
* its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
|
|
14218
|
+
* when the addon bundles no icon for that kind — the client then falls back
|
|
14219
|
+
* to a neutral glyph rather than rendering the raw `icon` NAME as text.
|
|
14220
|
+
*
|
|
14221
|
+
* Root-relative on purpose: it resolves against whatever origin serves a web
|
|
14222
|
+
* client, and a native client joins it onto its own hub base.
|
|
14223
|
+
*
|
|
14224
|
+
* DECLARED here deliberately. It used to travel as an undeclared passthrough
|
|
14225
|
+
* field that survived only because the runtime cap-router forwards provider
|
|
14226
|
+
* output verbatim — so every consumer had to re-declare it by hand to stop
|
|
14227
|
+
* its own Zod parse from stripping it, and the whole arrangement would have
|
|
14228
|
+
* broken silently the moment output validation was tightened anywhere.
|
|
14229
|
+
*/
|
|
14230
|
+
iconUrl: string().optional(),
|
|
14231
|
+
/**
|
|
14232
|
+
* Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
|
|
14233
|
+
*
|
|
14234
|
+
* The server knows this and therefore says it, because the client cannot
|
|
14235
|
+
* safely guess: a React-Native client renders SVG and raster through two
|
|
14236
|
+
* DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
|
|
14237
|
+
* not decode SVG on iOS/Android), so without this it silently fell back to a
|
|
14238
|
+
* placeholder glyph for every vector icon while the web build looked fine.
|
|
14239
|
+
*
|
|
14240
|
+
* Absent when {@link iconUrl} is absent, or for a legacy provider that has
|
|
14241
|
+
* not been updated — a client that cannot determine the type should prefer
|
|
14242
|
+
* its raster path, which is the safe default for an unknown image.
|
|
14243
|
+
*/
|
|
14244
|
+
iconMediaType: string().optional(),
|
|
14245
|
+
configSchema: ConfigSchemaPassthrough$4,
|
|
14246
|
+
/**
|
|
14247
|
+
* The config keys that decide WHERE this kind's secrets are sent (server,
|
|
14248
|
+
* base URL, instance, chat, recipient). A save that changes any of them
|
|
14249
|
+
* while leaving a secret redacted is refused by the provider (D634), so an
|
|
14250
|
+
* editor uses this to ask for the secret on exactly those edits instead of
|
|
14251
|
+
* on every edit.
|
|
14252
|
+
*
|
|
14253
|
+
* Optional: absent from a provider that holds no secret (the Home Assistant
|
|
14254
|
+
* kinds) and from a provider older than this field — an editor that finds
|
|
14255
|
+
* it absent must assume ANY edit may need the secret.
|
|
14256
|
+
*/
|
|
14257
|
+
destinationKeys: array(string()).optional(),
|
|
14258
|
+
supportsDiscovery: boolean(),
|
|
14259
|
+
/**
|
|
14260
|
+
* Extra delivery parameters a rule may set for a target of this kind (D938).
|
|
14261
|
+
* Absent = none; the rule editor then offers nothing.
|
|
14262
|
+
*/
|
|
14263
|
+
deliveryParams: array(DeliveryParamSchema).optional(),
|
|
14264
|
+
caps: TargetKindCapsSchema
|
|
14265
|
+
});
|
|
14266
|
+
/**
|
|
14267
|
+
* A persisted target. `config` holds secrets; providers REDACT secret fields
|
|
14268
|
+
* (return a presence marker only) when serving `listTargets` — never
|
|
14269
|
+
* round-trip a stored secret to the UI.
|
|
14270
|
+
*/
|
|
14271
|
+
var TargetSchema = object({
|
|
14272
|
+
id: string(),
|
|
14273
|
+
name: string(),
|
|
14274
|
+
kind: string(),
|
|
14275
|
+
addonId: string(),
|
|
14276
|
+
enabled: boolean(),
|
|
14277
|
+
config: record(string(), unknown())
|
|
14278
|
+
});
|
|
14279
|
+
/** A discovery-surfaced candidate (config is partial + non-secret). */
|
|
14280
|
+
var DiscoveredTargetSchema = object({
|
|
14281
|
+
kind: string(),
|
|
14282
|
+
suggestedName: string(),
|
|
14283
|
+
config: record(string(), unknown())
|
|
14284
|
+
});
|
|
14285
|
+
/** The degrade engine's report — what was resolved / dropped / degraded. */
|
|
14286
|
+
var RenderedAsSchema = object({
|
|
14287
|
+
level: string(),
|
|
14288
|
+
format: NotificationFormatSchema,
|
|
14289
|
+
attachmentsSent: number().int().nonnegative(),
|
|
14290
|
+
actionsSent: number().int().nonnegative(),
|
|
14291
|
+
truncated: boolean(),
|
|
14292
|
+
dropped: array(string())
|
|
14293
|
+
});
|
|
14294
|
+
var SendResultSchema = object({
|
|
14295
|
+
success: boolean(),
|
|
14296
|
+
error: string().optional(),
|
|
14297
|
+
renderedAs: RenderedAsSchema.optional()
|
|
14298
|
+
});
|
|
14299
|
+
/** Same shape as SendResult — kept as a distinct name for the test panel. */
|
|
14300
|
+
var TestResultSchema = SendResultSchema;
|
|
14301
|
+
/**
|
|
14302
|
+
* Pre-save credential probe against a partial (unsaved) target config.
|
|
14303
|
+
* `resolvedUrl` never includes secrets (token stripped from path).
|
|
14304
|
+
*/
|
|
14305
|
+
var ProbeTargetConfigResultSchema = object({
|
|
14306
|
+
ok: boolean(),
|
|
14307
|
+
resolvedUrl: string(),
|
|
14308
|
+
httpStatus: number().int().optional(),
|
|
14309
|
+
stateInstance: string().optional(),
|
|
14310
|
+
error: string().optional()
|
|
14311
|
+
});
|
|
14312
|
+
method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
|
|
14313
|
+
kind: string(),
|
|
14314
|
+
config: record(string(), unknown()).optional(),
|
|
14315
|
+
/**
|
|
14316
|
+
* When editing / duplicating, the blank password field never round-trips
|
|
14317
|
+
* the stored token. Pass the source target id so the provider can restore
|
|
14318
|
+
* secrets server-side before dialling (same merge as upsert).
|
|
14319
|
+
*/
|
|
14320
|
+
targetId: string().optional()
|
|
14321
|
+
}), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
|
|
14322
|
+
kind: string(),
|
|
14323
|
+
config: record(string(), unknown()),
|
|
14324
|
+
/** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
|
|
14325
|
+
targetId: string().optional()
|
|
14326
|
+
}), ProbeTargetConfigResultSchema, {
|
|
14327
|
+
kind: "mutation",
|
|
14328
|
+
auth: "admin"
|
|
14329
|
+
}), method(object({
|
|
14330
|
+
targetId: string(),
|
|
14331
|
+
notification: NotificationSchema
|
|
14332
|
+
}), SendResultSchema, {
|
|
14333
|
+
kind: "mutation",
|
|
14334
|
+
auth: "admin"
|
|
14335
|
+
}), method(object({
|
|
14336
|
+
targetId: string(),
|
|
14337
|
+
sample: NotificationSchema.optional()
|
|
14338
|
+
}), TestResultSchema, {
|
|
14339
|
+
kind: "mutation",
|
|
14340
|
+
auth: "admin"
|
|
14341
|
+
}), method(object({
|
|
14342
|
+
target: TargetSchema,
|
|
14343
|
+
/**
|
|
14344
|
+
* Duplicate flow: create a NEW target id while copying secrets from an
|
|
14345
|
+
* existing row. The UI never holds the stored token; blank / redacted
|
|
14346
|
+
* password fields are restored from this source (D634 still applies if
|
|
14347
|
+
* destinations diverge without a re-typed secret).
|
|
14348
|
+
*/
|
|
14349
|
+
cloneSecretsFrom: string().optional()
|
|
14350
|
+
}), TargetSchema, {
|
|
14351
|
+
kind: "mutation",
|
|
14352
|
+
auth: "admin"
|
|
14353
|
+
}), method(object({ targetId: string() }), _void(), {
|
|
14354
|
+
kind: "mutation",
|
|
14355
|
+
auth: "admin"
|
|
14356
|
+
}), method(object({
|
|
14357
|
+
targetId: string(),
|
|
14358
|
+
enabled: boolean()
|
|
14359
|
+
}), _void(), {
|
|
14360
|
+
kind: "mutation",
|
|
14361
|
+
auth: "admin"
|
|
14362
|
+
});
|
|
14363
|
+
looseObject({
|
|
14364
|
+
name: string().min(1),
|
|
14365
|
+
enabled: boolean().optional()
|
|
14366
|
+
});
|
|
14367
|
+
TargetSchema.omit({ id: true });
|
|
13917
14368
|
var EncodeProfileSchema = object({
|
|
13918
14369
|
video: object({
|
|
13919
14370
|
codec: _enum([
|
|
@@ -22328,7 +22779,6 @@ var SYSTEM_CAP_VOCABULARY = {
|
|
|
22328
22779
|
"calendar-source": system(INTERNAL),
|
|
22329
22780
|
calendars: system("calendars feed schedules and conditions, not device actions"),
|
|
22330
22781
|
composer: system("device composition — an authoring tool, not an action"),
|
|
22331
|
-
"connection-test": system(INTERNAL),
|
|
22332
22782
|
"core-blocks": system(INTERNAL),
|
|
22333
22783
|
"custom-model-registry": system(ML),
|
|
22334
22784
|
"data-store-provider": system(INTERNAL),
|
|
@@ -44384,427 +44834,6 @@ var UnitWarningSchema = object({
|
|
|
44384
44834
|
assumedUnit: string(),
|
|
44385
44835
|
message: string()
|
|
44386
44836
|
});
|
|
44387
|
-
/**
|
|
44388
|
-
* notification-output — canonical, capability-gated notification delivery.
|
|
44389
|
-
*
|
|
44390
|
-
* Apprise-derived model (see
|
|
44391
|
-
* `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
|
|
44392
|
-
* callers emit ONE canonical `Notification`; each provider declares a
|
|
44393
|
-
* per-kind capability descriptor (`TargetKind`), and the pure degrade
|
|
44394
|
-
* engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
|
|
44395
|
-
* message to what the kind supports — callers never special-case a service.
|
|
44396
|
-
*
|
|
44397
|
-
* DESIGN DECISIONS (locked):
|
|
44398
|
-
* - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
|
|
44399
|
-
* `setTargetEnabled`), each provider persisting via the `settings-store`
|
|
44400
|
-
* cap. Rationale: the admin UI needs one uniform surface across the
|
|
44401
|
-
* notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
|
|
44402
|
-
* alternative would fork the UI per addon and cannot host the
|
|
44403
|
-
* discovery→adopt flow.
|
|
44404
|
-
* - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
|
|
44405
|
-
* the generated cap-mount auto-`concatCollection`-fans them across every
|
|
44406
|
-
* registered provider (notifiers addon + HA addon) so one catalog is
|
|
44407
|
-
* routable. `send` / `testTarget` / CRUD route to ONE provider by the
|
|
44408
|
-
* `addonId` the generated collection router extracts from the call input.
|
|
44409
|
-
* - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
|
|
44410
|
-
* transformer) + UDS MsgPack both round-trip typed arrays — already used by
|
|
44411
|
-
* `storage` / `storage-provider` / `recording` caps over the same path. No
|
|
44412
|
-
* base64 fallback needed.
|
|
44413
|
-
*
|
|
44414
|
-
* TODO (deferred, closed-set change — separate decision): add
|
|
44415
|
-
* `providerKind: 'notify'` so notification providers surface on the unified
|
|
44416
|
-
* admin "Integrations" page.
|
|
44417
|
-
*/
|
|
44418
|
-
/**
|
|
44419
|
-
* Zentik-derived typed-media enum — the superset across every kind. Each
|
|
44420
|
-
* adapter picks what it supports and the degrade engine filters the rest.
|
|
44421
|
-
*/
|
|
44422
|
-
var AttachmentMediaTypeSchema = _enum([
|
|
44423
|
-
"image",
|
|
44424
|
-
"video",
|
|
44425
|
-
"gif",
|
|
44426
|
-
"audio",
|
|
44427
|
-
"icon"
|
|
44428
|
-
]);
|
|
44429
|
-
/**
|
|
44430
|
-
* A single attachment. Exactly one of `url` (remote source, most adapters
|
|
44431
|
-
* prefer this) or `bytes` (inline source; required for Pushover-style
|
|
44432
|
-
* bytes-only kinds) MUST be present — the degrade engine expresses a
|
|
44433
|
-
* url→bytes fetch as a `needsFetch` directive the adapter executes.
|
|
44434
|
-
*/
|
|
44435
|
-
var AttachmentSchema = object({
|
|
44436
|
-
mediaType: AttachmentMediaTypeSchema,
|
|
44437
|
-
url: string().optional(),
|
|
44438
|
-
bytes: _instanceof(Uint8Array).optional(),
|
|
44439
|
-
mime: string().optional(),
|
|
44440
|
-
name: string().optional()
|
|
44441
|
-
}).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
|
|
44442
|
-
var NotificationFormatSchema = _enum([
|
|
44443
|
-
"text",
|
|
44444
|
-
"markdown",
|
|
44445
|
-
"html"
|
|
44446
|
-
]);
|
|
44447
|
-
/**
|
|
44448
|
-
* The CLOSED icon vocabulary an action button may use.
|
|
44449
|
-
*
|
|
44450
|
-
* A closed set, not a free string, and that is the whole point: an arbitrary
|
|
44451
|
-
* icon name is one that ntfy renders, zentik silently drops, and nobody
|
|
44452
|
-
* notices — the same class of gap as a zone vocabulary nothing produced
|
|
44453
|
-
* ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
|
|
44454
|
-
* declares `actionIcons: false` and the degrade engine strips the field.
|
|
44455
|
-
*
|
|
44456
|
-
* Named by INTENT, never by glyph. "check" would tie the vocabulary to one
|
|
44457
|
-
* renderer's icon set; "acknowledge" survives an adapter that draws it
|
|
44458
|
-
* differently.
|
|
44459
|
-
*
|
|
44460
|
-
* ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
|
|
44461
|
-
*
|
|
44462
|
-
* These names are for US. **No adapter may forward one verbatim.** Each maps
|
|
44463
|
-
* the whole set onto its own renderer's vocabulary through a
|
|
44464
|
-
* `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
|
|
44465
|
-
* fallback, so adding a member here fails every adapter's build until someone
|
|
44466
|
-
* decides its glyph, which is the only place that decision can be made
|
|
44467
|
-
* honestly.
|
|
44468
|
-
*
|
|
44469
|
-
* This paragraph is the bug. Zentik declared `actionIcons: true` and passed
|
|
44470
|
-
* `disarm` straight through; iOS feeds that string to
|
|
44471
|
-
* `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
|
|
44472
|
-
* and every snooze and alarm button arrived BLANK. A pass-through is not a
|
|
44473
|
-
* mapping, and "the field is documented" is not "the value renders".
|
|
44474
|
-
*
|
|
44475
|
-
* Adding a member is TRAIN-BOUND. The enum lives in the published
|
|
44476
|
-
* `@camstack/server` closure and the cap seam validates against the HUB's copy,
|
|
44477
|
-
* so an addon that emits a token the running hub does not know does not lose an
|
|
44478
|
-
* icon — its whole `send` fails Zod validation and the notification never
|
|
44479
|
-
* arrives. Never emit a new token from an addon before the train carrying it.
|
|
44480
|
-
*/
|
|
44481
|
-
var NotificationActionIconSchema = _enum([
|
|
44482
|
-
"acknowledge",
|
|
44483
|
-
"dismiss",
|
|
44484
|
-
"silence",
|
|
44485
|
-
"snooze",
|
|
44486
|
-
"view",
|
|
44487
|
-
"play",
|
|
44488
|
-
"open",
|
|
44489
|
-
"close",
|
|
44490
|
-
"lock",
|
|
44491
|
-
"unlock",
|
|
44492
|
-
"arm",
|
|
44493
|
-
"arm-home",
|
|
44494
|
-
"arm-away",
|
|
44495
|
-
"arm-night",
|
|
44496
|
-
"disarm",
|
|
44497
|
-
"light",
|
|
44498
|
-
"alert",
|
|
44499
|
-
"camera"
|
|
44500
|
-
]);
|
|
44501
|
-
/** A single tap-through action button. */
|
|
44502
|
-
var NotificationActionSchema = object({
|
|
44503
|
-
id: string(),
|
|
44504
|
-
label: string(),
|
|
44505
|
-
url: string().optional(),
|
|
44506
|
-
/** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
|
|
44507
|
-
icon: NotificationActionIconSchema.optional(),
|
|
44508
|
-
/**
|
|
44509
|
-
* Renders in a warning style where the notifier supports it.
|
|
44510
|
-
*
|
|
44511
|
-
* A HINT, never a gate. The callback's authority is its token and nothing
|
|
44512
|
-
* else — see `notification-center/action-token.ts` for what that does and
|
|
44513
|
-
* does not buy.
|
|
44514
|
-
*/
|
|
44515
|
-
destructive: boolean().optional(),
|
|
44516
|
-
/**
|
|
44517
|
-
* How the tap should REACH the url.
|
|
44518
|
-
*
|
|
44519
|
-
* `navigate` (absent, and every button authored before this field) opens it:
|
|
44520
|
-
* the phone leaves the notification and shows whatever the callback returns.
|
|
44521
|
-
* That is right for a button whose answer the operator wants to read.
|
|
44522
|
-
*
|
|
44523
|
-
* `background` fires it as a POST and stays put. It exists for the buttons
|
|
44524
|
-
* whose whole point is not to interrupt — "silence this for 30 minutes" is
|
|
44525
|
-
* an answer to the notification, and being thrown into a browser tab to
|
|
44526
|
-
* confirm it costs more attention than the notification did. A backend that
|
|
44527
|
-
* cannot do a background call renders it as an ordinary link (the adapters
|
|
44528
|
-
* fall back rather than dropping the button), so this is a preference, never
|
|
44529
|
-
* a requirement.
|
|
44530
|
-
*/
|
|
44531
|
-
mode: _enum(["navigate", "background"]).optional()
|
|
44532
|
-
});
|
|
44533
|
-
/** A delivery parameter value: what a notifier declared it takes. */
|
|
44534
|
-
var DeliveryParamValueSchema = union([
|
|
44535
|
-
string(),
|
|
44536
|
-
number(),
|
|
44537
|
-
boolean()
|
|
44538
|
-
]);
|
|
44539
|
-
/**
|
|
44540
|
-
* One extra delivery parameter a notifier KIND declares (D938). A rule that
|
|
44541
|
-
* addresses a target of that kind may set it; a kind that declares nothing is
|
|
44542
|
-
* never handed one. The admin UI derives its control from this descriptor —
|
|
44543
|
-
* no editor knows a service by name.
|
|
44544
|
-
*
|
|
44545
|
-
* Optional fields are `.optional()`, never `.default()` (a Zod default does not
|
|
44546
|
-
* run on the addon cap path).
|
|
44547
|
-
*/
|
|
44548
|
-
var DeliveryParamSchema = object({
|
|
44549
|
-
key: string().min(1),
|
|
44550
|
-
label: string(),
|
|
44551
|
-
/** i18n key of the label, resolved by the declaring addon's namespace. */
|
|
44552
|
-
labelKey: string().optional(),
|
|
44553
|
-
type: _enum([
|
|
44554
|
-
"enum",
|
|
44555
|
-
"number",
|
|
44556
|
-
"boolean",
|
|
44557
|
-
"string"
|
|
44558
|
-
]),
|
|
44559
|
-
/** `enum` only: the closed set of accepted values. */
|
|
44560
|
-
options: array(object({
|
|
44561
|
-
value: string(),
|
|
44562
|
-
label: string()
|
|
44563
|
-
})).optional(),
|
|
44564
|
-
/** `number` only. */
|
|
44565
|
-
min: number().optional(),
|
|
44566
|
-
max: number().optional(),
|
|
44567
|
-
/** What the notifier does when the rule sets nothing; shown as the placeholder. */
|
|
44568
|
-
default: DeliveryParamValueSchema.optional()
|
|
44569
|
-
});
|
|
44570
|
-
/**
|
|
44571
|
-
* The canonical notification. `body` is the only hard field (Apprise model).
|
|
44572
|
-
* `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
|
|
44573
|
-
* NOT a fixed severity enum — each kind declares its own `caps.levels` and
|
|
44574
|
-
* the adapter maps this ordinal onto its native level. `level?` is an
|
|
44575
|
-
* optional kind-native level id (`emergency`, `silent`, …) that overrides
|
|
44576
|
-
* `priority` for that one target.
|
|
44577
|
-
*/
|
|
44578
|
-
var NotificationSchema = object({
|
|
44579
|
-
body: string(),
|
|
44580
|
-
title: string().optional(),
|
|
44581
|
-
format: NotificationFormatSchema.default("text"),
|
|
44582
|
-
priority: number().int().min(1).max(5).default(3),
|
|
44583
|
-
level: string().optional(),
|
|
44584
|
-
attachments: array(AttachmentSchema).optional(),
|
|
44585
|
-
clickUrl: string().optional(),
|
|
44586
|
-
actions: array(NotificationActionSchema).optional(),
|
|
44587
|
-
sound: string().optional(),
|
|
44588
|
-
ttl: number().optional(),
|
|
44589
|
-
tag: string().optional(),
|
|
44590
|
-
deviceId: number().optional(),
|
|
44591
|
-
eventId: string().optional(),
|
|
44592
|
-
metadata: record(string(), unknown()).optional(),
|
|
44593
|
-
/**
|
|
44594
|
-
* Values for the parameters the target's kind DECLARES in
|
|
44595
|
-
* {@link TargetKindSchema} `deliveryParams`. Already validated against the
|
|
44596
|
-
* declaration by the sender; a notifier reads only the keys it declared.
|
|
44597
|
-
*/
|
|
44598
|
-
delivery: record(string(), DeliveryParamValueSchema).optional()
|
|
44599
|
-
});
|
|
44600
|
-
/** One declared native severity/priority level for a kind. */
|
|
44601
|
-
var TargetKindLevelSchema = object({
|
|
44602
|
-
id: string(),
|
|
44603
|
-
label: string(),
|
|
44604
|
-
/** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
|
|
44605
|
-
ordinal: number().int().min(1).max(5).nullable(),
|
|
44606
|
-
flags: object({
|
|
44607
|
-
critical: boolean().optional(),
|
|
44608
|
-
silent: boolean().optional(),
|
|
44609
|
-
noPush: boolean().optional()
|
|
44610
|
-
}).optional(),
|
|
44611
|
-
/** e.g. Pushover `emergency` requires `retry` / `expire`. */
|
|
44612
|
-
requires: array(string()).optional(),
|
|
44613
|
-
description: string().optional()
|
|
44614
|
-
});
|
|
44615
|
-
/** The full capability block consulted before dispatch. */
|
|
44616
|
-
var TargetKindCapsSchema = object({
|
|
44617
|
-
attachments: object({
|
|
44618
|
-
mediaTypes: array(AttachmentMediaTypeSchema),
|
|
44619
|
-
mode: _enum([
|
|
44620
|
-
"url",
|
|
44621
|
-
"bytes",
|
|
44622
|
-
"both"
|
|
44623
|
-
]),
|
|
44624
|
-
max: number().int().nonnegative(),
|
|
44625
|
-
maxBytes: number().int().positive().optional()
|
|
44626
|
-
}),
|
|
44627
|
-
/** Max action buttons (0 = none). */
|
|
44628
|
-
actions: number().int().nonnegative(),
|
|
44629
|
-
/**
|
|
44630
|
-
* Whether this kind renders a per-action ICON.
|
|
44631
|
-
*
|
|
44632
|
-
* `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
|
|
44633
|
-
* run on the addon cap path — three production failures in one day taught
|
|
44634
|
-
* this repo that once. Absent is read as false by the degrade engine, which
|
|
44635
|
-
* is the safe direction: an icon that is not rendered costs nothing, an icon
|
|
44636
|
-
* assumed and dropped costs the operator's trust in the field.
|
|
44637
|
-
*/
|
|
44638
|
-
actionIcons: boolean().optional(),
|
|
44639
|
-
levels: array(TargetKindLevelSchema),
|
|
44640
|
-
format: array(NotificationFormatSchema),
|
|
44641
|
-
clickUrl: boolean(),
|
|
44642
|
-
sound: boolean(),
|
|
44643
|
-
ttl: boolean(),
|
|
44644
|
-
bodyMaxLen: number().int().positive()
|
|
44645
|
-
});
|
|
44646
|
-
/**
|
|
44647
|
-
* `configSchema` is a `ConfigUISchema` tree passed through to the admin
|
|
44648
|
-
* FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
|
|
44649
|
-
* `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
|
|
44650
|
-
* the union is large and not meant for runtime validation here; the exported
|
|
44651
|
-
* `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
|
|
44652
|
-
*/
|
|
44653
|
-
var ConfigSchemaPassthrough$4 = unknown();
|
|
44654
|
-
var TargetKindSchema = object({
|
|
44655
|
-
kind: string(),
|
|
44656
|
-
label: string(),
|
|
44657
|
-
icon: string(),
|
|
44658
|
-
/** Stamped by each provider so the concat-fanned catalog stays routable. */
|
|
44659
|
-
addonId: string(),
|
|
44660
|
-
/**
|
|
44661
|
-
* URL of the kind's bundled BRAND icon, served by the providing addon over
|
|
44662
|
-
* its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
|
|
44663
|
-
* when the addon bundles no icon for that kind — the client then falls back
|
|
44664
|
-
* to a neutral glyph rather than rendering the raw `icon` NAME as text.
|
|
44665
|
-
*
|
|
44666
|
-
* Root-relative on purpose: it resolves against whatever origin serves a web
|
|
44667
|
-
* client, and a native client joins it onto its own hub base.
|
|
44668
|
-
*
|
|
44669
|
-
* DECLARED here deliberately. It used to travel as an undeclared passthrough
|
|
44670
|
-
* field that survived only because the runtime cap-router forwards provider
|
|
44671
|
-
* output verbatim — so every consumer had to re-declare it by hand to stop
|
|
44672
|
-
* its own Zod parse from stripping it, and the whole arrangement would have
|
|
44673
|
-
* broken silently the moment output validation was tightened anywhere.
|
|
44674
|
-
*/
|
|
44675
|
-
iconUrl: string().optional(),
|
|
44676
|
-
/**
|
|
44677
|
-
* Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
|
|
44678
|
-
*
|
|
44679
|
-
* The server knows this and therefore says it, because the client cannot
|
|
44680
|
-
* safely guess: a React-Native client renders SVG and raster through two
|
|
44681
|
-
* DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
|
|
44682
|
-
* not decode SVG on iOS/Android), so without this it silently fell back to a
|
|
44683
|
-
* placeholder glyph for every vector icon while the web build looked fine.
|
|
44684
|
-
*
|
|
44685
|
-
* Absent when {@link iconUrl} is absent, or for a legacy provider that has
|
|
44686
|
-
* not been updated — a client that cannot determine the type should prefer
|
|
44687
|
-
* its raster path, which is the safe default for an unknown image.
|
|
44688
|
-
*/
|
|
44689
|
-
iconMediaType: string().optional(),
|
|
44690
|
-
configSchema: ConfigSchemaPassthrough$4,
|
|
44691
|
-
/**
|
|
44692
|
-
* The config keys that decide WHERE this kind's secrets are sent (server,
|
|
44693
|
-
* base URL, instance, chat, recipient). A save that changes any of them
|
|
44694
|
-
* while leaving a secret redacted is refused by the provider (D634), so an
|
|
44695
|
-
* editor uses this to ask for the secret on exactly those edits instead of
|
|
44696
|
-
* on every edit.
|
|
44697
|
-
*
|
|
44698
|
-
* Optional: absent from a provider that holds no secret (the Home Assistant
|
|
44699
|
-
* kinds) and from a provider older than this field — an editor that finds
|
|
44700
|
-
* it absent must assume ANY edit may need the secret.
|
|
44701
|
-
*/
|
|
44702
|
-
destinationKeys: array(string()).optional(),
|
|
44703
|
-
supportsDiscovery: boolean(),
|
|
44704
|
-
/**
|
|
44705
|
-
* Extra delivery parameters a rule may set for a target of this kind (D938).
|
|
44706
|
-
* Absent = none; the rule editor then offers nothing.
|
|
44707
|
-
*/
|
|
44708
|
-
deliveryParams: array(DeliveryParamSchema).optional(),
|
|
44709
|
-
caps: TargetKindCapsSchema
|
|
44710
|
-
});
|
|
44711
|
-
/**
|
|
44712
|
-
* A persisted target. `config` holds secrets; providers REDACT secret fields
|
|
44713
|
-
* (return a presence marker only) when serving `listTargets` — never
|
|
44714
|
-
* round-trip a stored secret to the UI.
|
|
44715
|
-
*/
|
|
44716
|
-
var TargetSchema = object({
|
|
44717
|
-
id: string(),
|
|
44718
|
-
name: string(),
|
|
44719
|
-
kind: string(),
|
|
44720
|
-
addonId: string(),
|
|
44721
|
-
enabled: boolean(),
|
|
44722
|
-
config: record(string(), unknown())
|
|
44723
|
-
});
|
|
44724
|
-
/** A discovery-surfaced candidate (config is partial + non-secret). */
|
|
44725
|
-
var DiscoveredTargetSchema = object({
|
|
44726
|
-
kind: string(),
|
|
44727
|
-
suggestedName: string(),
|
|
44728
|
-
config: record(string(), unknown())
|
|
44729
|
-
});
|
|
44730
|
-
/** The degrade engine's report — what was resolved / dropped / degraded. */
|
|
44731
|
-
var RenderedAsSchema = object({
|
|
44732
|
-
level: string(),
|
|
44733
|
-
format: NotificationFormatSchema,
|
|
44734
|
-
attachmentsSent: number().int().nonnegative(),
|
|
44735
|
-
actionsSent: number().int().nonnegative(),
|
|
44736
|
-
truncated: boolean(),
|
|
44737
|
-
dropped: array(string())
|
|
44738
|
-
});
|
|
44739
|
-
var SendResultSchema = object({
|
|
44740
|
-
success: boolean(),
|
|
44741
|
-
error: string().optional(),
|
|
44742
|
-
renderedAs: RenderedAsSchema.optional()
|
|
44743
|
-
});
|
|
44744
|
-
/** Same shape as SendResult — kept as a distinct name for the test panel. */
|
|
44745
|
-
var TestResultSchema = SendResultSchema;
|
|
44746
|
-
/**
|
|
44747
|
-
* Pre-save credential probe against a partial (unsaved) target config.
|
|
44748
|
-
* `resolvedUrl` never includes secrets (token stripped from path).
|
|
44749
|
-
*/
|
|
44750
|
-
var ProbeTargetConfigResultSchema = object({
|
|
44751
|
-
ok: boolean(),
|
|
44752
|
-
resolvedUrl: string(),
|
|
44753
|
-
httpStatus: number().int().optional(),
|
|
44754
|
-
stateInstance: string().optional(),
|
|
44755
|
-
error: string().optional()
|
|
44756
|
-
});
|
|
44757
|
-
method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
|
|
44758
|
-
kind: string(),
|
|
44759
|
-
config: record(string(), unknown()).optional(),
|
|
44760
|
-
/**
|
|
44761
|
-
* When editing / duplicating, the blank password field never round-trips
|
|
44762
|
-
* the stored token. Pass the source target id so the provider can restore
|
|
44763
|
-
* secrets server-side before dialling (same merge as upsert).
|
|
44764
|
-
*/
|
|
44765
|
-
targetId: string().optional()
|
|
44766
|
-
}), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
|
|
44767
|
-
kind: string(),
|
|
44768
|
-
config: record(string(), unknown()),
|
|
44769
|
-
/** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
|
|
44770
|
-
targetId: string().optional()
|
|
44771
|
-
}), ProbeTargetConfigResultSchema, {
|
|
44772
|
-
kind: "mutation",
|
|
44773
|
-
auth: "admin"
|
|
44774
|
-
}), method(object({
|
|
44775
|
-
targetId: string(),
|
|
44776
|
-
notification: NotificationSchema
|
|
44777
|
-
}), SendResultSchema, {
|
|
44778
|
-
kind: "mutation",
|
|
44779
|
-
auth: "admin"
|
|
44780
|
-
}), method(object({
|
|
44781
|
-
targetId: string(),
|
|
44782
|
-
sample: NotificationSchema.optional()
|
|
44783
|
-
}), TestResultSchema, {
|
|
44784
|
-
kind: "mutation",
|
|
44785
|
-
auth: "admin"
|
|
44786
|
-
}), method(object({
|
|
44787
|
-
target: TargetSchema,
|
|
44788
|
-
/**
|
|
44789
|
-
* Duplicate flow: create a NEW target id while copying secrets from an
|
|
44790
|
-
* existing row. The UI never holds the stored token; blank / redacted
|
|
44791
|
-
* password fields are restored from this source (D634 still applies if
|
|
44792
|
-
* destinations diverge without a re-typed secret).
|
|
44793
|
-
*/
|
|
44794
|
-
cloneSecretsFrom: string().optional()
|
|
44795
|
-
}), TargetSchema, {
|
|
44796
|
-
kind: "mutation",
|
|
44797
|
-
auth: "admin"
|
|
44798
|
-
}), method(object({ targetId: string() }), _void(), {
|
|
44799
|
-
kind: "mutation",
|
|
44800
|
-
auth: "admin"
|
|
44801
|
-
}), method(object({
|
|
44802
|
-
targetId: string(),
|
|
44803
|
-
enabled: boolean()
|
|
44804
|
-
}), _void(), {
|
|
44805
|
-
kind: "mutation",
|
|
44806
|
-
auth: "admin"
|
|
44807
|
-
});
|
|
44808
44837
|
_enum([
|
|
44809
44838
|
"automation",
|
|
44810
44839
|
"action",
|
|
@@ -49344,37 +49373,6 @@ method(object({
|
|
|
49344
49373
|
kind: "mutation",
|
|
49345
49374
|
auth: "admin"
|
|
49346
49375
|
}), method(object({ blockIds: array(string()) }), object({ states: array(CompositionBlockStateSchema) }), { auth: "admin" });
|
|
49347
|
-
var ConnectionTestOutcomeSchema = discriminatedUnion("outcome", [
|
|
49348
|
-
object({
|
|
49349
|
-
outcome: literal("validated"),
|
|
49350
|
-
/** Round-trip of the sign-in, when the provider measured it. */
|
|
49351
|
-
latencyMs: number().nonnegative().optional(),
|
|
49352
|
-
/** Optional human detail worth showing next to the tick
|
|
49353
|
-
* ("3 devices visible on this account"). */
|
|
49354
|
-
detail: string().optional()
|
|
49355
|
-
}).strict(),
|
|
49356
|
-
object({
|
|
49357
|
-
outcome: literal("rejected"),
|
|
49358
|
-
error: string()
|
|
49359
|
-
}).strict(),
|
|
49360
|
-
object({
|
|
49361
|
-
outcome: literal("inconclusive"),
|
|
49362
|
-
error: string()
|
|
49363
|
-
}).strict()
|
|
49364
|
-
]);
|
|
49365
|
-
var ConnectionTestInputSchema = object({
|
|
49366
|
-
/** Candidate integration settings, exactly as the create form collected them. */
|
|
49367
|
-
settings: record(string(), unknown()) });
|
|
49368
|
-
/**
|
|
49369
|
-
* What the provider's test actually DOES, so the UI can say it in words before
|
|
49370
|
-
* the operator presses the button ("Signs in to the Dreo cloud"). Purely
|
|
49371
|
-
* descriptive — it never changes routing.
|
|
49372
|
-
*/
|
|
49373
|
-
var ConnectionTestDescriptorSchema = object({ label: string() });
|
|
49374
|
-
method(ConnectionTestInputSchema, ConnectionTestOutcomeSchema, {
|
|
49375
|
-
kind: "mutation",
|
|
49376
|
-
auth: "admin"
|
|
49377
|
-
}), method(_void(), ConnectionTestDescriptorSchema, { auth: "admin" });
|
|
49378
49376
|
/**
|
|
49379
49377
|
* Upstream-system connectivity sensor — distinct from `device-status`,
|
|
49380
49378
|
* which is the kernel-managed online/offline flag for the device's
|
|
@@ -56549,10 +56547,9 @@ var AvailableIntegrationTypeSchema = object({
|
|
|
56549
56547
|
* locations" checkbox. Provider-declared in the addon manifest. */
|
|
56550
56548
|
supportsLocationImport: boolean(),
|
|
56551
56549
|
/**
|
|
56552
|
-
* True when this integration
|
|
56553
|
-
*
|
|
56554
|
-
*
|
|
56555
|
-
* rather than offering a button that always answers the same nonsense.
|
|
56550
|
+
* True when this integration's instances declare a Test (`integration-instances.describe().canTest`,
|
|
56551
|
+
* D1016). An integration that cannot be tested must say so up front rather than offering a
|
|
56552
|
+
* button that always answers the same nonsense.
|
|
56556
56553
|
*/
|
|
56557
56554
|
canTest: boolean(),
|
|
56558
56555
|
existingInstances: array(object({
|
|
@@ -56606,7 +56603,9 @@ var CreateIntegrationInputSchema = object({
|
|
|
56606
56603
|
info: record(string(), unknown()).optional(),
|
|
56607
56604
|
/** `stableId` of the discovery candidate the form was seeded from (D984). */
|
|
56608
56605
|
originStableId: string().optional(),
|
|
56609
|
-
settings: record(string(), unknown()).optional()
|
|
56606
|
+
settings: record(string(), unknown()).optional(),
|
|
56607
|
+
/** The instance kind being created, for a provider with several; omitted = its only testable kind. */
|
|
56608
|
+
kind: string().min(1).optional()
|
|
56610
56609
|
});
|
|
56611
56610
|
var UpdateIntegrationInputSchema = object({
|
|
56612
56611
|
id: string(),
|
|
@@ -56632,13 +56631,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
56632
56631
|
}), method(object({ id: string() }), DeleteIntegrationResultSchema, {
|
|
56633
56632
|
kind: "mutation",
|
|
56634
56633
|
auth: "admin"
|
|
56635
|
-
}), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" })
|
|
56636
|
-
addonId: string(),
|
|
56637
|
-
settings: record(string(), unknown())
|
|
56638
|
-
}), TestConnectionResultSchema, {
|
|
56639
|
-
kind: "mutation",
|
|
56640
|
-
auth: "admin"
|
|
56641
|
-
});
|
|
56634
|
+
}), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" });
|
|
56642
56635
|
/**
|
|
56643
56636
|
* `integration-instances` - the connection of an addon that holds ONE or SEVERAL
|
|
56644
56637
|
* (an account, a relay, a destination) as typed records (D985, D986). The addon's
|
|
@@ -65381,13 +65374,6 @@ var AddBrokerInputSchema = object({
|
|
|
65381
65374
|
});
|
|
65382
65375
|
var AddBrokerResultSchema = object({ id: string() });
|
|
65383
65376
|
var IdInputSchema = object({ id: string() });
|
|
65384
|
-
var TestResultSchema$1 = discriminatedUnion("ok", [object({
|
|
65385
|
-
ok: literal(true),
|
|
65386
|
-
latencyMs: number()
|
|
65387
|
-
}), object({
|
|
65388
|
-
ok: literal(false),
|
|
65389
|
-
error: string()
|
|
65390
|
-
})]);
|
|
65391
65377
|
var StartEmbeddedInputSchema = object({
|
|
65392
65378
|
port: number().int().min(1).max(65535).default(1883),
|
|
65393
65379
|
/** Allow anonymous connect (no username/password). Default: false. */
|
|
@@ -65410,9 +65396,6 @@ method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnection
|
|
|
65410
65396
|
}), method(IdInputSchema, _void(), {
|
|
65411
65397
|
kind: "mutation",
|
|
65412
65398
|
auth: "admin"
|
|
65413
|
-
}), method(IdInputSchema, TestResultSchema$1, {
|
|
65414
|
-
kind: "mutation",
|
|
65415
|
-
auth: "admin"
|
|
65416
65399
|
}), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
|
|
65417
65400
|
kind: "mutation",
|
|
65418
65401
|
auth: "admin"
|
|
@@ -78519,7 +78502,7 @@ var TrainingProviderTypeSchema = object({
|
|
|
78519
78502
|
label: string(),
|
|
78520
78503
|
/** Path under the addon package (e.g. `assets/training-modal/icon.svg`). */
|
|
78521
78504
|
iconAsset: string(),
|
|
78522
|
-
/** Owning `training-provider` addon
|
|
78505
|
+
/** Owning `training-provider` addon. */
|
|
78523
78506
|
addonId: string(),
|
|
78524
78507
|
/**
|
|
78525
78508
|
* Fields for one entry of this type (credentials + defaults). Absent on a
|
|
@@ -78564,36 +78547,6 @@ var TrainingProviderDescriptorSchema = object({
|
|
|
78564
78547
|
*/
|
|
78565
78548
|
formValues: record(string(), unknown()).optional()
|
|
78566
78549
|
});
|
|
78567
|
-
/**
|
|
78568
|
-
* Patch one entry's name, credentials and defaults. Pin with owning `{ addonId }`.
|
|
78569
|
-
* An omitted or empty `tokenSecret` means KEEP the stored secret (D787).
|
|
78570
|
-
* `{ $secretClear: true }` clears it.
|
|
78571
|
-
*/
|
|
78572
|
-
var TrainingProviderUpdateEntryInputSchema = object({
|
|
78573
|
-
entryId: string().min(1),
|
|
78574
|
-
name: string().min(1),
|
|
78575
|
-
tokenId: string().optional(),
|
|
78576
|
-
tokenSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
|
|
78577
|
-
environment: string().optional(),
|
|
78578
|
-
gpuType: string().optional(),
|
|
78579
|
-
region: string().optional(),
|
|
78580
|
-
costCapUsd: number().nonnegative().optional(),
|
|
78581
|
-
maxRuntimeSec: number().int().positive().optional(),
|
|
78582
|
-
failedDatasetGraceHours: number().nonnegative().optional()
|
|
78583
|
-
});
|
|
78584
|
-
var TrainingCredentialCheckSchema = object({
|
|
78585
|
-
ok: boolean(),
|
|
78586
|
-
/** Workspace / account name, when the provider says. */
|
|
78587
|
-
account: string().optional(),
|
|
78588
|
-
reason: _enum([
|
|
78589
|
-
"missing",
|
|
78590
|
-
"invalid",
|
|
78591
|
-
"forbidden",
|
|
78592
|
-
"network",
|
|
78593
|
-
"quota"
|
|
78594
|
-
]).optional(),
|
|
78595
|
-
detail: string().optional()
|
|
78596
|
-
});
|
|
78597
78550
|
var ProviderJobStateSchema = _enum([
|
|
78598
78551
|
"pending",
|
|
78599
78552
|
"starting",
|
|
@@ -78641,22 +78594,6 @@ var TrainingLogLineSchema = object({
|
|
|
78641
78594
|
text: string()
|
|
78642
78595
|
});
|
|
78643
78596
|
method(object({}), array(TrainingProviderDescriptorSchema).readonly(), { auth: "admin" }), method(object({}), array(TrainingProviderTypeSchema).readonly(), { auth: "admin" }), method(object({}), array(TrainingProviderEntrySchema).readonly(), { auth: "admin" }), method(object({
|
|
78644
|
-
typeId: string().min(1),
|
|
78645
|
-
name: string().min(1)
|
|
78646
|
-
}), object({ entryId: string() }), {
|
|
78647
|
-
kind: "mutation",
|
|
78648
|
-
auth: "admin"
|
|
78649
|
-
}), method(object({ entryId: string().min(1) }), object({ ok: literal(true) }), {
|
|
78650
|
-
kind: "mutation",
|
|
78651
|
-
auth: "admin"
|
|
78652
|
-
}), method(TrainingProviderUpdateEntryInputSchema, object({ ok: literal(true) }), {
|
|
78653
|
-
kind: "mutation",
|
|
78654
|
-
auth: "admin"
|
|
78655
|
-
}), method(object({ entryId: string().min(1) }), TrainingCredentialCheckSchema, {
|
|
78656
|
-
kind: "mutation",
|
|
78657
|
-
auth: "admin",
|
|
78658
|
-
timeoutMs: 3e4
|
|
78659
|
-
}), method(object({
|
|
78660
78597
|
entryId: string().min(1),
|
|
78661
78598
|
gpuType: string(),
|
|
78662
78599
|
region: string(),
|
|
@@ -84109,20 +84046,6 @@ Object.freeze({
|
|
|
84109
84046
|
access: "view",
|
|
84110
84047
|
role: "admin"
|
|
84111
84048
|
},
|
|
84112
|
-
"connectionTest.describeTest": {
|
|
84113
|
-
capName: "connection-test",
|
|
84114
|
-
capScope: "system",
|
|
84115
|
-
addonId: null,
|
|
84116
|
-
access: "view",
|
|
84117
|
-
role: "admin"
|
|
84118
|
-
},
|
|
84119
|
-
"connectionTest.testSettings": {
|
|
84120
|
-
capName: "connection-test",
|
|
84121
|
-
capScope: "system",
|
|
84122
|
-
addonId: null,
|
|
84123
|
-
access: "create",
|
|
84124
|
-
role: "admin"
|
|
84125
|
-
},
|
|
84126
84049
|
"connectivity.getStatus": {
|
|
84127
84050
|
capName: "connectivity",
|
|
84128
84051
|
capScope: "device",
|
|
@@ -86489,13 +86412,6 @@ Object.freeze({
|
|
|
86489
86412
|
access: "view",
|
|
86490
86413
|
role: "admin"
|
|
86491
86414
|
},
|
|
86492
|
-
"integrations.testConnection": {
|
|
86493
|
-
capName: "integrations",
|
|
86494
|
-
capScope: "system",
|
|
86495
|
-
addonId: null,
|
|
86496
|
-
access: "create",
|
|
86497
|
-
role: "admin"
|
|
86498
|
-
},
|
|
86499
86415
|
"integrations.update": {
|
|
86500
86416
|
capName: "integrations",
|
|
86501
86417
|
capScope: "system",
|
|
@@ -87728,13 +87644,6 @@ Object.freeze({
|
|
|
87728
87644
|
access: "create",
|
|
87729
87645
|
role: "admin"
|
|
87730
87646
|
},
|
|
87731
|
-
"mqttBroker.testConnection": {
|
|
87732
|
-
capName: "mqtt-broker",
|
|
87733
|
-
capScope: "system",
|
|
87734
|
-
addonId: null,
|
|
87735
|
-
access: "create",
|
|
87736
|
-
role: "admin"
|
|
87737
|
-
},
|
|
87738
87647
|
"nativeObjectDetection.getOptions": {
|
|
87739
87648
|
capName: "native-object-detection",
|
|
87740
87649
|
capScope: "device",
|
|
@@ -92565,20 +92474,6 @@ Object.freeze({
|
|
|
92565
92474
|
access: "create",
|
|
92566
92475
|
role: "admin"
|
|
92567
92476
|
},
|
|
92568
|
-
"trainingProvider.createEntry": {
|
|
92569
|
-
capName: "training-provider",
|
|
92570
|
-
capScope: "system",
|
|
92571
|
-
addonId: null,
|
|
92572
|
-
access: "create",
|
|
92573
|
-
role: "admin"
|
|
92574
|
-
},
|
|
92575
|
-
"trainingProvider.deleteEntry": {
|
|
92576
|
-
capName: "training-provider",
|
|
92577
|
-
capScope: "system",
|
|
92578
|
-
addonId: null,
|
|
92579
|
-
access: "delete",
|
|
92580
|
-
role: "admin"
|
|
92581
|
-
},
|
|
92582
92477
|
"trainingProvider.describe": {
|
|
92583
92478
|
capName: "training-provider",
|
|
92584
92479
|
capScope: "system",
|
|
@@ -92649,13 +92544,6 @@ Object.freeze({
|
|
|
92649
92544
|
access: "create",
|
|
92650
92545
|
role: "admin"
|
|
92651
92546
|
},
|
|
92652
|
-
"trainingProvider.updateEntry": {
|
|
92653
|
-
capName: "training-provider",
|
|
92654
|
-
capScope: "system",
|
|
92655
|
-
addonId: null,
|
|
92656
|
-
access: "create",
|
|
92657
|
-
role: "admin"
|
|
92658
|
-
},
|
|
92659
92547
|
"trainingProvider.uploadDataset": {
|
|
92660
92548
|
capName: "training-provider",
|
|
92661
92549
|
capScope: "system",
|
|
@@ -92663,13 +92551,6 @@ Object.freeze({
|
|
|
92663
92551
|
access: "create",
|
|
92664
92552
|
role: "admin"
|
|
92665
92553
|
},
|
|
92666
|
-
"trainingProvider.validateCredentials": {
|
|
92667
|
-
capName: "training-provider",
|
|
92668
|
-
capScope: "system",
|
|
92669
|
-
addonId: null,
|
|
92670
|
-
access: "create",
|
|
92671
|
-
role: "admin"
|
|
92672
|
-
},
|
|
92673
92554
|
"turnProvider.getTurnServers": {
|
|
92674
92555
|
capName: "turn-provider",
|
|
92675
92556
|
capScope: "system",
|