@camstack/types 1.2.379 → 1.2.381
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/dist/addon.d.ts +3 -0
- package/dist/addon.js +67 -2
- package/dist/addon.mjs +5 -3
- package/dist/capabilities/caller-stamp.d.ts +12 -0
- package/dist/capabilities/index.d.ts +7 -2
- package/dist/capabilities/record-entries.cap.d.ts +84 -0
- package/dist/capabilities/records.cap.d.ts +399 -0
- package/dist/capabilities/storage-admin.cap.d.ts +268 -0
- package/dist/client.js +57 -32
- package/dist/client.mjs +57 -32
- package/dist/device/device-context.d.ts +2 -0
- package/dist/ffmpeg/fmp4-fragment-child.d.ts +2 -2
- package/dist/ffmpeg/process.d.ts +21 -0
- package/dist/generated/addon-api.d.ts +191 -0
- package/dist/generated/cap-input-defaults.d.ts +1 -1
- package/dist/generated/capability-router-map.d.ts +8 -2
- package/dist/generated/method-access-map.d.ts +1 -1
- package/dist/generated/method-device-selectors.d.ts +2 -2
- package/dist/generated/system-proxy.d.ts +2 -0
- package/dist/helpers/addon-slice-handle.d.ts +4 -0
- package/dist/helpers/trpc-leaf.d.ts +17 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +724 -32
- package/dist/index.mjs +646 -33
- package/dist/interfaces/addon-peer-bytes.d.ts +19 -2
- package/dist/interfaces/addon.d.ts +13 -0
- package/dist/interfaces/config-ui.d.ts +4 -0
- package/dist/interfaces/event-bus.d.ts +32 -3
- package/dist/interfaces/storage.d.ts +1 -1
- package/dist/node.d.ts +2 -2
- package/dist/node.js +4 -0
- package/dist/node.mjs +4 -1
- package/dist/records/constants.d.ts +23 -0
- package/dist/records/entry-kind.d.ts +31 -0
- package/dist/records/errors.d.ts +81 -0
- package/dist/records/in-memory-records.d.ts +21 -0
- package/dist/records/index.d.ts +9 -0
- package/dist/records/record-kind.d.ts +144 -0
- package/dist/records/records-access.d.ts +153 -0
- package/dist/records/secret-paths.d.ts +52 -0
- package/dist/records/settings-array-adopts-from.d.ts +12 -0
- package/dist/records/settings-brokers-adopts-from.d.ts +10 -0
- package/dist/{sleep-DQYi915G.js → sleep-D8yJVc4r.js} +1971 -290
- package/dist/{sleep-D-Q2_Mcd.mjs → sleep-bZIhUvjd.mjs} +1413 -128
- package/package.json +1 -1
|
@@ -162,7 +162,7 @@ function collectKeychainStoredKeys(schema) {
|
|
|
162
162
|
return keys;
|
|
163
163
|
}
|
|
164
164
|
function walkKeychainField(field, out) {
|
|
165
|
-
if (!isRecord$
|
|
165
|
+
if (!isRecord$6(field)) return;
|
|
166
166
|
const type = field["type"];
|
|
167
167
|
const key = field["key"];
|
|
168
168
|
const isSecret = type === "password" || field["secret"] === true;
|
|
@@ -187,21 +187,21 @@ function schemaDeclaresAnyField(schema) {
|
|
|
187
187
|
for (const section of sectionsOf(schema)) if (fieldsOf(section).length > 0) return true;
|
|
188
188
|
return false;
|
|
189
189
|
}
|
|
190
|
-
function isRecord$
|
|
190
|
+
function isRecord$6(value) {
|
|
191
191
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
192
192
|
}
|
|
193
193
|
function sectionsOf(schema) {
|
|
194
|
-
if (!isRecord$
|
|
194
|
+
if (!isRecord$6(schema)) return [];
|
|
195
195
|
const sections = schema["sections"];
|
|
196
196
|
return Array.isArray(sections) ? sections : [];
|
|
197
197
|
}
|
|
198
198
|
function fieldsOf(node) {
|
|
199
|
-
if (!isRecord$
|
|
199
|
+
if (!isRecord$6(node)) return [];
|
|
200
200
|
const fields = node["fields"];
|
|
201
201
|
return Array.isArray(fields) ? fields : [];
|
|
202
202
|
}
|
|
203
203
|
function walkField(field, out) {
|
|
204
|
-
if (!isRecord$
|
|
204
|
+
if (!isRecord$6(field)) return;
|
|
205
205
|
const type = field["type"];
|
|
206
206
|
const key = field["key"];
|
|
207
207
|
if ((type === "password" || field["secret"] === true) && typeof key === "string" && key.length > 0) out.add(key);
|
|
@@ -241,7 +241,7 @@ function walkField(field, out) {
|
|
|
241
241
|
var SECRET_CLEAR = { $secretClear: true };
|
|
242
242
|
/** The `'***'` token D634 redaction writes for non-admins. */
|
|
243
243
|
var LEGACY_DEVICE_REDACTION = "***";
|
|
244
|
-
function isRecord$
|
|
244
|
+
function isRecord$5(value) {
|
|
245
245
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
246
246
|
}
|
|
247
247
|
function makeSecretMarker(ref) {
|
|
@@ -252,19 +252,19 @@ function makeSecretMarker(ref) {
|
|
|
252
252
|
}
|
|
253
253
|
/** A stored marker: an object whose `$secret` is a non-empty string. */
|
|
254
254
|
function isSecretMarker(value) {
|
|
255
|
-
if (!isRecord$
|
|
255
|
+
if (!isRecord$5(value)) return false;
|
|
256
256
|
const ref = value["$secret"];
|
|
257
257
|
return typeof ref === "string" && ref.length > 0;
|
|
258
258
|
}
|
|
259
259
|
/** Anything carrying a `$secret` key — a stored marker or a UI view of one. */
|
|
260
260
|
function isSecretMarkerLike(value) {
|
|
261
|
-
return isRecord$
|
|
261
|
+
return isRecord$5(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
|
|
262
262
|
}
|
|
263
263
|
function isSecretClear(value) {
|
|
264
|
-
return isRecord$
|
|
264
|
+
return isRecord$5(value) && value["$secretClear"] === true;
|
|
265
265
|
}
|
|
266
266
|
function isSecretFieldView(value) {
|
|
267
|
-
return isRecord$
|
|
267
|
+
return isRecord$5(value) && value["set"] === true && isSecretMarkerLike(value);
|
|
268
268
|
}
|
|
269
269
|
/**
|
|
270
270
|
* The UI view of a stored secret field value: a marker becomes "set", inline
|
|
@@ -677,6 +677,33 @@ function typeOf(field) {
|
|
|
677
677
|
default: return "other";
|
|
678
678
|
}
|
|
679
679
|
}
|
|
680
|
+
/** Strip inline `value` from a hydrated field for schema-only round-trips. */
|
|
681
|
+
function dehydrateConfigField(field) {
|
|
682
|
+
if (field.type === "group") return {
|
|
683
|
+
...field,
|
|
684
|
+
fields: field.fields.map(dehydrateConfigField)
|
|
685
|
+
};
|
|
686
|
+
if (field.type === "sub-tabs") return {
|
|
687
|
+
...field,
|
|
688
|
+
tabs: field.tabs.map((tab) => ({
|
|
689
|
+
...tab,
|
|
690
|
+
fields: tab.fields.map(dehydrateConfigField)
|
|
691
|
+
}))
|
|
692
|
+
};
|
|
693
|
+
if (isValuelessField(field)) return field;
|
|
694
|
+
const { value: _drop, ...rest } = field;
|
|
695
|
+
return rest;
|
|
696
|
+
}
|
|
697
|
+
/** Inverse of {@link hydrateSchema} — drops resolved values from every leaf field. */
|
|
698
|
+
function dehydrateSchema(schema) {
|
|
699
|
+
return {
|
|
700
|
+
...schema.tabs ? { tabs: [...schema.tabs] } : {},
|
|
701
|
+
sections: schema.sections.map((section) => ({
|
|
702
|
+
...section,
|
|
703
|
+
fields: section.fields.map(dehydrateConfigField)
|
|
704
|
+
}))
|
|
705
|
+
};
|
|
706
|
+
}
|
|
680
707
|
//#endregion
|
|
681
708
|
//#region src/interfaces/event-bus.ts
|
|
682
709
|
/**
|
|
@@ -1018,7 +1045,7 @@ var KeychainWriteRefusedError = class extends Error {
|
|
|
1018
1045
|
}
|
|
1019
1046
|
};
|
|
1020
1047
|
var KEEP = { kind: "keep" };
|
|
1021
|
-
function isRecord$
|
|
1048
|
+
function isRecord$4(value) {
|
|
1022
1049
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1023
1050
|
}
|
|
1024
1051
|
function defaultMintId() {
|
|
@@ -1031,7 +1058,7 @@ function collectSecretMarkerRefs(value, out = []) {
|
|
|
1031
1058
|
return out;
|
|
1032
1059
|
}
|
|
1033
1060
|
if (Array.isArray(value)) for (const item of value) collectSecretMarkerRefs(item, out);
|
|
1034
|
-
else if (isRecord$
|
|
1061
|
+
else if (isRecord$4(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
|
|
1035
1062
|
return out;
|
|
1036
1063
|
}
|
|
1037
1064
|
async function storedValueEquals(secrets, marker, candidate) {
|
|
@@ -1086,7 +1113,7 @@ function rowId(row, idKey) {
|
|
|
1086
1113
|
return null;
|
|
1087
1114
|
}
|
|
1088
1115
|
async function writeArray(ctx, node, incoming, stored, fieldPath) {
|
|
1089
|
-
const storedRows = Array.isArray(stored) ? stored.filter(isRecord$
|
|
1116
|
+
const storedRows = Array.isArray(stored) ? stored.filter(isRecord$4) : [];
|
|
1090
1117
|
if (!Array.isArray(incoming)) {
|
|
1091
1118
|
ctx.deletes.push(...collectSecretMarkerRefs(storedRows));
|
|
1092
1119
|
return {
|
|
@@ -1102,7 +1129,7 @@ async function writeArray(ctx, node, incoming, stored, fieldPath) {
|
|
|
1102
1129
|
const kept = /* @__PURE__ */ new Set();
|
|
1103
1130
|
const rows = [];
|
|
1104
1131
|
for (const row of incoming) {
|
|
1105
|
-
if (!isRecord$
|
|
1132
|
+
if (!isRecord$4(row)) {
|
|
1106
1133
|
rows.push(row);
|
|
1107
1134
|
continue;
|
|
1108
1135
|
}
|
|
@@ -1127,7 +1154,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
|
|
|
1127
1154
|
case "leaf": return writeLeaf(ctx, incoming, stored, fieldPath);
|
|
1128
1155
|
case "array": return writeArray(ctx, node, incoming, stored, fieldPath);
|
|
1129
1156
|
case "object": {
|
|
1130
|
-
if (!isRecord$
|
|
1157
|
+
if (!isRecord$4(incoming)) {
|
|
1131
1158
|
if (incoming === void 0) return KEEP;
|
|
1132
1159
|
ctx.deletes.push(...collectSecretMarkerRefs(stored));
|
|
1133
1160
|
return {
|
|
@@ -1135,7 +1162,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
|
|
|
1135
1162
|
value: incoming
|
|
1136
1163
|
};
|
|
1137
1164
|
}
|
|
1138
|
-
const storedObj = isRecord$
|
|
1165
|
+
const storedObj = isRecord$4(stored) ? stored : {};
|
|
1139
1166
|
return {
|
|
1140
1167
|
kind: "value",
|
|
1141
1168
|
value: await writeMembers(ctx, node.children, incoming, storedObj, (m) => `${fieldPath}.${m}`)
|
|
@@ -1258,7 +1285,7 @@ async function resolveSecretMarkers(value, secrets, options = {}) {
|
|
|
1258
1285
|
value: items
|
|
1259
1286
|
};
|
|
1260
1287
|
}
|
|
1261
|
-
if (isRecord$
|
|
1288
|
+
if (isRecord$4(v)) {
|
|
1262
1289
|
const out = {};
|
|
1263
1290
|
for (const [k, child] of Object.entries(v)) {
|
|
1264
1291
|
const s = substitute(child);
|
|
@@ -1292,7 +1319,7 @@ var SecretOverwriteRefusedError = class extends Error {
|
|
|
1292
1319
|
this.name = "SecretOverwriteRefusedError";
|
|
1293
1320
|
}
|
|
1294
1321
|
};
|
|
1295
|
-
function isRecord(value) {
|
|
1322
|
+
function isRecord$3(value) {
|
|
1296
1323
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1297
1324
|
}
|
|
1298
1325
|
function resolvedValue(pass, marker) {
|
|
@@ -1321,7 +1348,7 @@ function inArray(pass, incoming, stored) {
|
|
|
1321
1348
|
return incoming.map((item, index) => {
|
|
1322
1349
|
const twin = stored[index];
|
|
1323
1350
|
if (isSecretMarker(twin)) return typeof item === "string" && item === resolvedValue(pass, twin) ? twin : item;
|
|
1324
|
-
if (isRecord(item) && isRecord(twin)) return inArrayRow(pass, item, twin);
|
|
1351
|
+
if (isRecord$3(item) && isRecord$3(twin)) return inArrayRow(pass, item, twin);
|
|
1325
1352
|
if (Array.isArray(item) && Array.isArray(twin)) return inArray(pass, item, twin);
|
|
1326
1353
|
return item;
|
|
1327
1354
|
});
|
|
@@ -1332,7 +1359,7 @@ function inArrayRow(pass, incoming, stored) {
|
|
|
1332
1359
|
const twin = stored[key];
|
|
1333
1360
|
if (isSecretMarker(twin)) {
|
|
1334
1361
|
if (typeof value === "string" && value === resolvedValue(pass, twin)) out[key] = twin;
|
|
1335
|
-
} else if (isRecord(value) && isRecord(twin)) out[key] = inArrayRow(pass, value, twin);
|
|
1362
|
+
} else if (isRecord$3(value) && isRecord$3(twin)) out[key] = inArrayRow(pass, value, twin);
|
|
1336
1363
|
else if (Array.isArray(value) && Array.isArray(twin)) out[key] = inArray(pass, value, twin);
|
|
1337
1364
|
}
|
|
1338
1365
|
return out;
|
|
@@ -1340,7 +1367,7 @@ function inArrayRow(pass, incoming, stored) {
|
|
|
1340
1367
|
async function walk(pass, incoming, stored) {
|
|
1341
1368
|
if (isSecretMarker(stored)) return atMarker(pass, incoming, stored);
|
|
1342
1369
|
if (Array.isArray(incoming) && Array.isArray(stored)) return inArray(pass, incoming, stored);
|
|
1343
|
-
if (!isRecord(incoming) || !isRecord(stored)) return incoming;
|
|
1370
|
+
if (!isRecord$3(incoming) || !isRecord$3(stored)) return incoming;
|
|
1344
1371
|
const out = { ...incoming };
|
|
1345
1372
|
for (const [key, value] of Object.entries(incoming)) out[key] = await walk(pass, value, stored[key]);
|
|
1346
1373
|
return out;
|
|
@@ -1371,7 +1398,7 @@ async function restoreUnchangedSecrets(input) {
|
|
|
1371
1398
|
function isJsonEqual(a, b) {
|
|
1372
1399
|
if (Object.is(a, b)) return true;
|
|
1373
1400
|
if (Array.isArray(a)) return Array.isArray(b) && a.length === b.length && a.every((v, i) => isJsonEqual(v, b[i]));
|
|
1374
|
-
if (isRecord(a) && isRecord(b)) {
|
|
1401
|
+
if (isRecord$3(a) && isRecord$3(b)) {
|
|
1375
1402
|
const keys = Object.keys(a);
|
|
1376
1403
|
if (keys.length !== Object.keys(b).length) return false;
|
|
1377
1404
|
return keys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && isJsonEqual(a[k], b[k]));
|
|
@@ -1395,7 +1422,7 @@ function markerKeysOf(blob, owner) {
|
|
|
1395
1422
|
}
|
|
1396
1423
|
function nodeFromStored(value, owner) {
|
|
1397
1424
|
if (isSecretMarker(value)) return isSecretRefOwnedBy(value.$secret, owner) ? { kind: "leaf" } : null;
|
|
1398
|
-
if (!isRecord(value)) return null;
|
|
1425
|
+
if (!isRecord$3(value)) return null;
|
|
1399
1426
|
const children = /* @__PURE__ */ new Map();
|
|
1400
1427
|
for (const [key, child] of Object.entries(value)) {
|
|
1401
1428
|
const node = nodeFromStored(child, owner);
|
|
@@ -2202,7 +2229,7 @@ var BaseAddon = class {
|
|
|
2202
2229
|
if (!ctx) return;
|
|
2203
2230
|
const addonId = ctx.id;
|
|
2204
2231
|
setImmediate(() => {
|
|
2205
|
-
ctx.api.addons
|
|
2232
|
+
ctx.api.addons.restartAddon.mutate({ addonId }).then(() => {
|
|
2206
2233
|
ctx.logger.info("addon auto-restart triggered by restart-required setting change", { meta: { changedFields: patchKeys.filter((k) => restartKeys.has(k)) } });
|
|
2207
2234
|
}).catch((err) => {
|
|
2208
2235
|
ctx.logger.error("addon auto-restart failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
@@ -3122,6 +3149,1297 @@ async function resolveSecretViewsIn(args, secrets) {
|
|
|
3122
3149
|
return swap(args).value;
|
|
3123
3150
|
}
|
|
3124
3151
|
//#endregion
|
|
3152
|
+
//#region src/records/constants.ts
|
|
3153
|
+
/** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
|
|
3154
|
+
/** The files a kind may live in. `history` is device-history's (D850), never records. */
|
|
3155
|
+
var RECORD_DATABASES = ["settings", "events"];
|
|
3156
|
+
/** The classes of a kind: the `@durable` vocabulary, one authority (replaces the annotation). */
|
|
3157
|
+
var RECORD_CLASSES = [
|
|
3158
|
+
"config",
|
|
3159
|
+
"ledger",
|
|
3160
|
+
"audit",
|
|
3161
|
+
"mirror",
|
|
3162
|
+
"ephemeral"
|
|
3163
|
+
];
|
|
3164
|
+
var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
|
|
3165
|
+
var RECORD_MAX_DATA_BYTES = 1024 * 1024;
|
|
3166
|
+
var RECORD_ID_MAX_LENGTH = 256;
|
|
3167
|
+
var RECORD_BATCH_MAX = 1e3;
|
|
3168
|
+
var RECORD_BATCH_MAX_BYTES = 4 * 1024 * 1024;
|
|
3169
|
+
var RECORD_QUERY_MAX = 1e3;
|
|
3170
|
+
/** Rows one DELETE statement removes before the serving thread yields. */
|
|
3171
|
+
var RECORD_DELETE_BATCH = 5e3;
|
|
3172
|
+
/** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
|
|
3173
|
+
var RECORD_SETTINGS_MAX_ROWS = 5e4;
|
|
3174
|
+
var RECORD_MAX_INDEXES = 8;
|
|
3175
|
+
var RECORD_MAX_INDEX_FIELDS = 4;
|
|
3176
|
+
/** The engine's TTL sweep cadence. */
|
|
3177
|
+
var RECORD_SWEEP_INTERVAL_MS = 10 * 6e4;
|
|
3178
|
+
/** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
|
|
3179
|
+
var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
3180
|
+
//#endregion
|
|
3181
|
+
//#region src/records/errors.ts
|
|
3182
|
+
/**
|
|
3183
|
+
* The named failures of `ctx.records` (D881).
|
|
3184
|
+
*
|
|
3185
|
+
* A records call crosses a worker thread, a UDS channel and a tRPC router, and
|
|
3186
|
+
* an error class does not survive any of them - only its message does. So
|
|
3187
|
+
* every message STARTS with a stable code, `records/<code>: <detail>`, and
|
|
3188
|
+
* {@link recordsErrorFromMessage} rebuilds the typed class on the caller's side.
|
|
3189
|
+
* A failed read is never `[]` or `undefined`: it throws one of these.
|
|
3190
|
+
*/
|
|
3191
|
+
/** Every code a records failure carries. */
|
|
3192
|
+
var RECORDS_ERROR_CODES = [
|
|
3193
|
+
"rev-conflict",
|
|
3194
|
+
"too-large",
|
|
3195
|
+
"unindexed-filter",
|
|
3196
|
+
"filter-required",
|
|
3197
|
+
"unknown-kind",
|
|
3198
|
+
"not-found",
|
|
3199
|
+
"unique-violation",
|
|
3200
|
+
"plaintext-secret",
|
|
3201
|
+
"foreign-marker",
|
|
3202
|
+
"secret-unavailable",
|
|
3203
|
+
"invalid-record",
|
|
3204
|
+
"declaration-refused",
|
|
3205
|
+
"adoption-pending",
|
|
3206
|
+
"no-caller",
|
|
3207
|
+
"batch-too-large"
|
|
3208
|
+
];
|
|
3209
|
+
var PREFIX = "records/";
|
|
3210
|
+
/** The base of every records failure. `detail` is the human part of the message. */
|
|
3211
|
+
var RecordsError = class extends Error {
|
|
3212
|
+
code;
|
|
3213
|
+
detail;
|
|
3214
|
+
constructor(code, detail) {
|
|
3215
|
+
super(`${PREFIX}${code}: ${detail}`);
|
|
3216
|
+
this.name = "RecordsError";
|
|
3217
|
+
this.code = code;
|
|
3218
|
+
this.detail = detail;
|
|
3219
|
+
}
|
|
3220
|
+
};
|
|
3221
|
+
/** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
|
|
3222
|
+
var RevConflictError = class extends RecordsError {
|
|
3223
|
+
constructor(detail) {
|
|
3224
|
+
super("rev-conflict", detail);
|
|
3225
|
+
this.name = "RevConflictError";
|
|
3226
|
+
}
|
|
3227
|
+
};
|
|
3228
|
+
/** `data` exceeds the kind's `maxDataBytes`. Bytes are never record fields (D613). */
|
|
3229
|
+
var RecordTooLargeError = class extends RecordsError {
|
|
3230
|
+
constructor(detail) {
|
|
3231
|
+
super("too-large", detail);
|
|
3232
|
+
this.name = "RecordTooLargeError";
|
|
3233
|
+
}
|
|
3234
|
+
};
|
|
3235
|
+
/** A filter, order or aggregate names a field no declared index serves. */
|
|
3236
|
+
var UnindexedFilterError = class extends RecordsError {
|
|
3237
|
+
constructor(detail) {
|
|
3238
|
+
super("unindexed-filter", detail);
|
|
3239
|
+
this.name = "UnindexedFilterError";
|
|
3240
|
+
}
|
|
3241
|
+
};
|
|
3242
|
+
/** A bulk delete with no predicate at all. */
|
|
3243
|
+
var FilterRequiredError = class extends RecordsError {
|
|
3244
|
+
constructor(detail) {
|
|
3245
|
+
super("filter-required", detail);
|
|
3246
|
+
this.name = "FilterRequiredError";
|
|
3247
|
+
}
|
|
3248
|
+
};
|
|
3249
|
+
/** The caller declared no such kind (another addon's kind is indistinguishable from none). */
|
|
3250
|
+
var UnknownRecordKindError = class extends RecordsError {
|
|
3251
|
+
constructor(detail) {
|
|
3252
|
+
super("unknown-kind", detail);
|
|
3253
|
+
this.name = "UnknownRecordKindError";
|
|
3254
|
+
}
|
|
3255
|
+
};
|
|
3256
|
+
var RecordNotFoundError = class extends RecordsError {
|
|
3257
|
+
constructor(detail) {
|
|
3258
|
+
super("not-found", detail);
|
|
3259
|
+
this.name = "RecordNotFoundError";
|
|
3260
|
+
}
|
|
3261
|
+
};
|
|
3262
|
+
/** A unique index refused the write. */
|
|
3263
|
+
var RecordUniqueViolationError = class extends RecordsError {
|
|
3264
|
+
constructor(detail) {
|
|
3265
|
+
super("unique-violation", detail);
|
|
3266
|
+
this.name = "RecordUniqueViolationError";
|
|
3267
|
+
}
|
|
3268
|
+
};
|
|
3269
|
+
/** A secret field held a plain string where only a keychain marker may be stored. */
|
|
3270
|
+
var PlaintextSecretError = class extends RecordsError {
|
|
3271
|
+
constructor(detail) {
|
|
3272
|
+
super("plaintext-secret", detail);
|
|
3273
|
+
this.name = "PlaintextSecretError";
|
|
3274
|
+
}
|
|
3275
|
+
};
|
|
3276
|
+
/** A marker whose ref is not the row's own (the D878 rule). */
|
|
3277
|
+
var ForeignSecretMarkerError = class extends RecordsError {
|
|
3278
|
+
constructor(detail) {
|
|
3279
|
+
super("foreign-marker", detail);
|
|
3280
|
+
this.name = "ForeignSecretMarkerError";
|
|
3281
|
+
}
|
|
3282
|
+
};
|
|
3283
|
+
/** The keychain could not store or serve a value: explicit, never an empty string. */
|
|
3284
|
+
var RecordSecretUnavailableError = class extends RecordsError {
|
|
3285
|
+
constructor(detail) {
|
|
3286
|
+
super("secret-unavailable", detail);
|
|
3287
|
+
this.name = "RecordSecretUnavailableError";
|
|
3288
|
+
}
|
|
3289
|
+
};
|
|
3290
|
+
var InvalidRecordError = class extends RecordsError {
|
|
3291
|
+
constructor(detail) {
|
|
3292
|
+
super("invalid-record", detail);
|
|
3293
|
+
this.name = "InvalidRecordError";
|
|
3294
|
+
}
|
|
3295
|
+
};
|
|
3296
|
+
/** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
|
|
3297
|
+
var DeclarationRefusedError = class extends RecordsError {
|
|
3298
|
+
constructor(detail) {
|
|
3299
|
+
super("declaration-refused", detail);
|
|
3300
|
+
this.name = "DeclarationRefusedError";
|
|
3301
|
+
}
|
|
3302
|
+
};
|
|
3303
|
+
/** A kind is empty while the legacy table it adopts from still holds rows. */
|
|
3304
|
+
var AdoptionPendingError = class extends RecordsError {
|
|
3305
|
+
constructor(detail) {
|
|
3306
|
+
super("adoption-pending", detail);
|
|
3307
|
+
this.name = "AdoptionPendingError";
|
|
3308
|
+
}
|
|
3309
|
+
};
|
|
3310
|
+
var NoCallerError = class extends RecordsError {
|
|
3311
|
+
constructor(detail) {
|
|
3312
|
+
super("no-caller", detail);
|
|
3313
|
+
this.name = "NoCallerError";
|
|
3314
|
+
}
|
|
3315
|
+
};
|
|
3316
|
+
var BatchTooLargeError = class extends RecordsError {
|
|
3317
|
+
constructor(detail) {
|
|
3318
|
+
super("batch-too-large", detail);
|
|
3319
|
+
this.name = "BatchTooLargeError";
|
|
3320
|
+
}
|
|
3321
|
+
};
|
|
3322
|
+
var FACTORIES = {
|
|
3323
|
+
"rev-conflict": (d) => new RevConflictError(d),
|
|
3324
|
+
"too-large": (d) => new RecordTooLargeError(d),
|
|
3325
|
+
"unindexed-filter": (d) => new UnindexedFilterError(d),
|
|
3326
|
+
"filter-required": (d) => new FilterRequiredError(d),
|
|
3327
|
+
"unknown-kind": (d) => new UnknownRecordKindError(d),
|
|
3328
|
+
"not-found": (d) => new RecordNotFoundError(d),
|
|
3329
|
+
"unique-violation": (d) => new RecordUniqueViolationError(d),
|
|
3330
|
+
"plaintext-secret": (d) => new PlaintextSecretError(d),
|
|
3331
|
+
"foreign-marker": (d) => new ForeignSecretMarkerError(d),
|
|
3332
|
+
"secret-unavailable": (d) => new RecordSecretUnavailableError(d),
|
|
3333
|
+
"invalid-record": (d) => new InvalidRecordError(d),
|
|
3334
|
+
"declaration-refused": (d) => new DeclarationRefusedError(d),
|
|
3335
|
+
"adoption-pending": (d) => new AdoptionPendingError(d),
|
|
3336
|
+
"no-caller": (d) => new NoCallerError(d),
|
|
3337
|
+
"batch-too-large": (d) => new BatchTooLargeError(d)
|
|
3338
|
+
};
|
|
3339
|
+
function isCode(value) {
|
|
3340
|
+
return RECORDS_ERROR_CODES.some((code) => code === value);
|
|
3341
|
+
}
|
|
3342
|
+
/**
|
|
3343
|
+
* Rebuild the typed error from a message that crossed a wire; `null` when the
|
|
3344
|
+
* message is not a records failure.
|
|
3345
|
+
*/
|
|
3346
|
+
function recordsErrorFromMessage(message) {
|
|
3347
|
+
if (!message.startsWith(PREFIX)) return null;
|
|
3348
|
+
const colon = message.indexOf(": ");
|
|
3349
|
+
if (colon < 0) return null;
|
|
3350
|
+
const code = message.slice(8, colon);
|
|
3351
|
+
if (!isCode(code)) return null;
|
|
3352
|
+
return FACTORIES[code](message.slice(colon + 2));
|
|
3353
|
+
}
|
|
3354
|
+
/** The records failure inside any thrown value (a wrapped tRPC error included), or the value. */
|
|
3355
|
+
function asRecordsError(err) {
|
|
3356
|
+
if (err instanceof RecordsError) return err;
|
|
3357
|
+
if (err instanceof Error) {
|
|
3358
|
+
const typed = recordsErrorFromMessage(err.message);
|
|
3359
|
+
if (typed !== null) return typed;
|
|
3360
|
+
}
|
|
3361
|
+
return err;
|
|
3362
|
+
}
|
|
3363
|
+
//#endregion
|
|
3364
|
+
//#region src/device/zod-secret.ts
|
|
3365
|
+
/**
|
|
3366
|
+
* Secret declaration on a Zod config schema (D783).
|
|
3367
|
+
*
|
|
3368
|
+
* A field is a secret because its schema SAYS so — `.meta({ secret: true })`,
|
|
3369
|
+
* spelled {@link SECRET_META} — never because its key looks like a credential.
|
|
3370
|
+
* It is the Zod twin of `secret: true` / `type: 'password'` on a config-UI
|
|
3371
|
+
* field (`isSecretConfigField`), and every consumer that treats secrets
|
|
3372
|
+
* specially reads it: the settings form (`zodEntriesToConfigUI`), the paths a
|
|
3373
|
+
* device advertises at `registerDevice` (`secretConfigPaths`, which the
|
|
3374
|
+
* non-admin redaction withholds at any depth), and the keychain later.
|
|
3375
|
+
*
|
|
3376
|
+
* Zod keeps `.meta()` in `z.globalRegistry`, which lives on `globalThis`, so a
|
|
3377
|
+
* declaration made by an addon's own copy of zod is visible here.
|
|
3378
|
+
*/
|
|
3379
|
+
/**
|
|
3380
|
+
* `{ secret: true }` — the one spelling of "this field holds a credential".
|
|
3381
|
+
*
|
|
3382
|
+
* Addons write the LITERAL `.meta({ secret: true })` rather than importing
|
|
3383
|
+
* this constant: an addon running against an older `@camstack/types` would
|
|
3384
|
+
* otherwise call `.meta(undefined)`, which in zod v4 is the metadata GETTER
|
|
3385
|
+
* and returns no schema at all.
|
|
3386
|
+
*/
|
|
3387
|
+
var SECRET_META = { secret: true };
|
|
3388
|
+
function defOf(schema) {
|
|
3389
|
+
const zodBag = Reflect.get(schema, "_zod");
|
|
3390
|
+
if (typeof zodBag !== "object" || zodBag === null) return {};
|
|
3391
|
+
const def = Reflect.get(zodBag, "def");
|
|
3392
|
+
if (typeof def !== "object" || def === null) return {};
|
|
3393
|
+
return def;
|
|
3394
|
+
}
|
|
3395
|
+
function declaredHere(schema) {
|
|
3396
|
+
return z.globalRegistry.get(schema)?.secret === true;
|
|
3397
|
+
}
|
|
3398
|
+
/** Wrapper kinds whose `innerType` is the field itself. */
|
|
3399
|
+
var WRAPPERS = new Set([
|
|
3400
|
+
"optional",
|
|
3401
|
+
"nullable",
|
|
3402
|
+
"default",
|
|
3403
|
+
"prefault",
|
|
3404
|
+
"catch",
|
|
3405
|
+
"readonly",
|
|
3406
|
+
"nonoptional"
|
|
3407
|
+
]);
|
|
3408
|
+
/**
|
|
3409
|
+
* The schema layers that make up ONE field: the schema itself and every
|
|
3410
|
+
* wrapper below it (optional / default / nullable / readonly / catch), plus
|
|
3411
|
+
* both sides of a pipe (`z.preprocess` puts the real schema on `out`,
|
|
3412
|
+
* `.transform` on `in`).
|
|
3413
|
+
*/
|
|
3414
|
+
function layersOf(schema) {
|
|
3415
|
+
const out = [];
|
|
3416
|
+
const queue = [schema];
|
|
3417
|
+
while (queue.length > 0) {
|
|
3418
|
+
const s = queue.shift();
|
|
3419
|
+
if (!s || out.includes(s)) continue;
|
|
3420
|
+
out.push(s);
|
|
3421
|
+
const def = defOf(s);
|
|
3422
|
+
if (def.type !== void 0 && WRAPPERS.has(def.type) && def.innerType) queue.push(def.innerType);
|
|
3423
|
+
if (def.type === "pipe") {
|
|
3424
|
+
if (def.in) queue.push(def.in);
|
|
3425
|
+
if (def.out) queue.push(def.out);
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
return out;
|
|
3429
|
+
}
|
|
3430
|
+
/** True when any layer of this field declares `.meta({ secret: true })`. */
|
|
3431
|
+
function isZodSecret(schema) {
|
|
3432
|
+
return layersOf(schema).some(declaredHere);
|
|
3433
|
+
}
|
|
3434
|
+
/**
|
|
3435
|
+
* Every declared secret path in a config schema: dotted keys, `[]` for "each
|
|
3436
|
+
* element of this array" (`connection.password`, `rows[].token`). A declared
|
|
3437
|
+
* object or array is reported as one path and not descended into — everything
|
|
3438
|
+
* under it is secret.
|
|
3439
|
+
*/
|
|
3440
|
+
function collectZodSecretPaths(schema, prefix = "") {
|
|
3441
|
+
if (isZodSecret(schema)) return prefix.length > 0 ? [prefix] : [];
|
|
3442
|
+
const paths = [];
|
|
3443
|
+
for (const layer of layersOf(schema)) {
|
|
3444
|
+
const def = defOf(layer);
|
|
3445
|
+
if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
|
|
3446
|
+
else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
|
|
3447
|
+
else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
|
|
3448
|
+
else if (def.type === "intersection") {
|
|
3449
|
+
if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
|
|
3450
|
+
if (def.right) paths.push(...collectZodSecretPaths(def.right, prefix));
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
return [...new Set(paths)];
|
|
3454
|
+
}
|
|
3455
|
+
//#endregion
|
|
3456
|
+
//#region src/records/secret-paths.ts
|
|
3457
|
+
/**
|
|
3458
|
+
* Secret fields of a record, addressed the way a Zod schema declares them
|
|
3459
|
+
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
3460
|
+
* (`auth.accessToken`, `headers[].value`).
|
|
3461
|
+
*
|
|
3462
|
+
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
3463
|
+
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
3464
|
+
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
3465
|
+
* leaf names its element: `headers[X-Api].value`.
|
|
3466
|
+
*
|
|
3467
|
+
* Pure and browser-safe. Nothing here mutates: every transform returns a new value.
|
|
3468
|
+
*/
|
|
3469
|
+
/** The array prefixes (declared form, e.g. `headers` or `a.headers`) of a path. */
|
|
3470
|
+
function parseSecretPath(path) {
|
|
3471
|
+
if (path.length === 0) throw new InvalidRecordError("an empty secret path");
|
|
3472
|
+
return path.split(".").map((raw) => {
|
|
3473
|
+
const each = raw.endsWith("[]");
|
|
3474
|
+
const key = each ? raw.slice(0, -2) : raw;
|
|
3475
|
+
if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
|
|
3476
|
+
return {
|
|
3477
|
+
key,
|
|
3478
|
+
each
|
|
3479
|
+
};
|
|
3480
|
+
});
|
|
3481
|
+
}
|
|
3482
|
+
/** The declared array prefixes of `path` that need an id key (`headers`, `a.rows`). */
|
|
3483
|
+
function secretArrayPrefixes(path) {
|
|
3484
|
+
const segments = parseSecretPath(path);
|
|
3485
|
+
const prefixes = [];
|
|
3486
|
+
const walked = [];
|
|
3487
|
+
for (const segment of segments) {
|
|
3488
|
+
walked.push(segment.key);
|
|
3489
|
+
if (segment.each) prefixes.push(walked.join("."));
|
|
3490
|
+
}
|
|
3491
|
+
return prefixes;
|
|
3492
|
+
}
|
|
3493
|
+
function isRecord$2(value) {
|
|
3494
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3495
|
+
}
|
|
3496
|
+
function elementId(element, idKey, declared, prefix) {
|
|
3497
|
+
const id = isRecord$2(element) ? element[idKey] : void 0;
|
|
3498
|
+
if (typeof id !== "string" || id.length === 0) throw new InvalidRecordError(`"${declared}": every element of "${prefix}" needs a non-empty string "${idKey}" to carry a secret`);
|
|
3499
|
+
return id;
|
|
3500
|
+
}
|
|
3501
|
+
/**
|
|
3502
|
+
* Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
|
|
3503
|
+
* visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
|
|
3504
|
+
* result is a new body, `data` is untouched.
|
|
3505
|
+
*/
|
|
3506
|
+
var REMOVE_LEAF = Symbol("remove-leaf");
|
|
3507
|
+
function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
|
|
3508
|
+
if (!isRecord$2(node)) return node;
|
|
3509
|
+
const segment = segments[index];
|
|
3510
|
+
if (segment === void 0) return node;
|
|
3511
|
+
if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
|
|
3512
|
+
const child = node[segment.key];
|
|
3513
|
+
const keys = [...walkedKeys, segment.key];
|
|
3514
|
+
const last = index === segments.length - 1;
|
|
3515
|
+
if (segment.each) {
|
|
3516
|
+
if (!Array.isArray(child)) return node;
|
|
3517
|
+
const prefix = keys.join(".");
|
|
3518
|
+
const idKey = arrayKeys[prefix];
|
|
3519
|
+
if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
|
|
3520
|
+
const mapped = child.map((element) => {
|
|
3521
|
+
const id = elementId(element, idKey, declared, prefix);
|
|
3522
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
|
|
3523
|
+
return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
|
|
3524
|
+
});
|
|
3525
|
+
return {
|
|
3526
|
+
...node,
|
|
3527
|
+
[segment.key]: mapped
|
|
3528
|
+
};
|
|
3529
|
+
}
|
|
3530
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
3531
|
+
if (last) {
|
|
3532
|
+
const replacement = visit({
|
|
3533
|
+
declared,
|
|
3534
|
+
concrete,
|
|
3535
|
+
value: child
|
|
3536
|
+
});
|
|
3537
|
+
if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
|
|
3538
|
+
return {
|
|
3539
|
+
...node,
|
|
3540
|
+
[segment.key]: replacement
|
|
3541
|
+
};
|
|
3542
|
+
}
|
|
3543
|
+
return {
|
|
3544
|
+
...node,
|
|
3545
|
+
[segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
|
|
3546
|
+
};
|
|
3547
|
+
}
|
|
3548
|
+
/** Replace/remove every present secret leaf; returns a NEW body. */
|
|
3549
|
+
function mapSecretLeaves(data, paths, arrayKeys, visit) {
|
|
3550
|
+
let current = data;
|
|
3551
|
+
for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
|
|
3552
|
+
return current;
|
|
3553
|
+
}
|
|
3554
|
+
/** Every present secret leaf, read-only. */
|
|
3555
|
+
function collectSecretLeaves(data, paths, arrayKeys) {
|
|
3556
|
+
const found = [];
|
|
3557
|
+
mapSecretLeaves(data, paths, arrayKeys, (leaf) => {
|
|
3558
|
+
found.push(leaf);
|
|
3559
|
+
return leaf.value;
|
|
3560
|
+
});
|
|
3561
|
+
return found;
|
|
3562
|
+
}
|
|
3563
|
+
/**
|
|
3564
|
+
* The keychain collection name a kind's secrets live under. Prefixed so a
|
|
3565
|
+
* record's refs never collide with the refs a legacy collection of the same name
|
|
3566
|
+
* already holds during an adoption.
|
|
3567
|
+
*/
|
|
3568
|
+
function recordSecretCollection(kind) {
|
|
3569
|
+
return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
|
|
3570
|
+
}
|
|
3571
|
+
/** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
|
|
3572
|
+
function recordSecretRef(owner, kind, rowId, concretePath) {
|
|
3573
|
+
return buildSecretRef({
|
|
3574
|
+
owner,
|
|
3575
|
+
scope: {
|
|
3576
|
+
kind: "row",
|
|
3577
|
+
collection: recordSecretCollection(kind),
|
|
3578
|
+
rowId
|
|
3579
|
+
},
|
|
3580
|
+
fieldPath: concretePath
|
|
3581
|
+
});
|
|
3582
|
+
}
|
|
3583
|
+
/** A concrete path built from a flat array item, re-exported for adoption tooling. */
|
|
3584
|
+
var recordArrayItemPath = arrayItemFieldPath;
|
|
3585
|
+
//#endregion
|
|
3586
|
+
//#region src/records/record-kind.ts
|
|
3587
|
+
/**
|
|
3588
|
+
* Record kinds (D881): the declaration an addon makes of one kind of fact, the
|
|
3589
|
+
* Zod row schema that describes it, and the helper that turns the two into the
|
|
3590
|
+
* JSON the engine stores.
|
|
3591
|
+
*
|
|
3592
|
+
* A kind is declared ONCE, next to the code that owns it, and the engine's
|
|
3593
|
+
* reconciler creates its indexes - nothing creates DDL at runtime from a write.
|
|
3594
|
+
* Everything the generic machinery needs (secret fields, the entry descriptor)
|
|
3595
|
+
* is DERIVED from the Zod schema: `.meta({ secret: true })` marks a credential,
|
|
3596
|
+
* never a field name (D879).
|
|
3597
|
+
*/
|
|
3598
|
+
/** A JSON field path: `trackId`, `auth.accessToken`. Charset-restricted: it is rendered into DDL. */
|
|
3599
|
+
var RecordFieldPathSchema = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,63}(\.[A-Za-z_][A-Za-z0-9_]{0,63}){0,3}$/);
|
|
3600
|
+
var RecordIndexSchema = z.object({
|
|
3601
|
+
name: z.string().regex(/^[a-z][a-z0-9_]{0,31}$/),
|
|
3602
|
+
fields: z.array(RecordFieldPathSchema).min(1).max(4),
|
|
3603
|
+
unique: z.boolean().optional()
|
|
3604
|
+
}).strict();
|
|
3605
|
+
var RecordRetentionSchema = z.union([
|
|
3606
|
+
z.object({
|
|
3607
|
+
ttlMs: z.number().int().positive(),
|
|
3608
|
+
from: z.enum(["at", "write"])
|
|
3609
|
+
}).strict(),
|
|
3610
|
+
z.object({
|
|
3611
|
+
maxRows: z.number().int().positive().optional(),
|
|
3612
|
+
maxRowsPerDevice: z.number().int().positive().optional()
|
|
3613
|
+
}).strict().refine((r) => r.maxRows !== void 0 || r.maxRowsPerDevice !== void 0, { message: "name maxRows, maxRowsPerDevice or both" }),
|
|
3614
|
+
z.object({
|
|
3615
|
+
owner: z.string().min(1),
|
|
3616
|
+
reason: z.string().min(1)
|
|
3617
|
+
}).strict(),
|
|
3618
|
+
z.object({ none: z.string().min(1) }).strict()
|
|
3619
|
+
]);
|
|
3620
|
+
/** The descriptor a generic entries UI reads (D881 "generic entries"). */
|
|
3621
|
+
var EntryDescriptorSchema = z.object({
|
|
3622
|
+
title: z.string().min(1),
|
|
3623
|
+
/** The record body field shown as the entry's name. */
|
|
3624
|
+
nameField: z.string().min(1),
|
|
3625
|
+
/** JSON Schema of the entry body; `secret: true` marks a password field. */
|
|
3626
|
+
fields: z.record(z.string(), z.unknown())
|
|
3627
|
+
}).strict();
|
|
3628
|
+
/** What crosses the wire and sits in the engine's catalog: JSON only, no Zod. */
|
|
3629
|
+
var RecordKindDeclarationSchema = z.object({
|
|
3630
|
+
database: z.enum(RECORD_DATABASES).default("settings"),
|
|
3631
|
+
class: z.enum(RECORD_CLASSES),
|
|
3632
|
+
writeTrigger: z.string().min(1),
|
|
3633
|
+
device: z.enum([
|
|
3634
|
+
"required",
|
|
3635
|
+
"optional",
|
|
3636
|
+
"none"
|
|
3637
|
+
]).default("none"),
|
|
3638
|
+
at: z.enum(["required", "none"]).default("none"),
|
|
3639
|
+
indexes: z.array(RecordIndexSchema).max(8).default([]),
|
|
3640
|
+
retention: RecordRetentionSchema,
|
|
3641
|
+
deviceRemoval: z.enum(["delete", "none"]).default("none"),
|
|
3642
|
+
maxDataBytes: z.number().int().positive().max(RECORD_MAX_DATA_BYTES).default(RECORD_DEFAULT_MAX_DATA_BYTES),
|
|
3643
|
+
/** Derived from the Zod schema; never written by hand. */
|
|
3644
|
+
secretPaths: z.array(z.string().min(1)).default([]),
|
|
3645
|
+
/** The id key of each array that carries a secret (`{ headers: 'name' }`). */
|
|
3646
|
+
secretArrayKeys: z.record(z.string(), z.string()).default({}),
|
|
3647
|
+
/** The legacy table this kind adopts from; the boot guard reads it (D853: no migration code). */
|
|
3648
|
+
adoptsFrom: z.string().min(1).optional(),
|
|
3649
|
+
/** Kinds that must agree through `transact` sit in one file under one family name. */
|
|
3650
|
+
transactFamily: z.string().min(1).optional(),
|
|
3651
|
+
entry: EntryDescriptorSchema.optional()
|
|
3652
|
+
}).strict();
|
|
3653
|
+
/** The kind names: lower-case, dash separated, short enough for an index name. */
|
|
3654
|
+
var RecordKindNameSchema = z.string().regex(/^[a-z][a-z0-9-]{0,47}$/);
|
|
3655
|
+
/** Cross-field rules a declaration must satisfy. Throws {@link DeclarationRefusedError}. */
|
|
3656
|
+
function assertValidDeclaration(name, decl) {
|
|
3657
|
+
const refuse = (why) => {
|
|
3658
|
+
throw new DeclarationRefusedError(`kind "${name}": ${why}`);
|
|
3659
|
+
};
|
|
3660
|
+
if (!RecordKindNameSchema.safeParse(name).success) refuse("the name must be lower-case-dashed");
|
|
3661
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3662
|
+
for (const index of decl.indexes) {
|
|
3663
|
+
if (seen.has(index.name)) refuse(`index name "${index.name}" is declared twice`);
|
|
3664
|
+
seen.add(index.name);
|
|
3665
|
+
}
|
|
3666
|
+
const { retention } = decl;
|
|
3667
|
+
const hasSecrets = decl.secretPaths.length > 0;
|
|
3668
|
+
if ("ttlMs" in retention) {
|
|
3669
|
+
if (retention.from === "at" && decl.at !== "required") refuse("a ttl from \"at\" needs at: \"required\"");
|
|
3670
|
+
if (hasSecrets) refuse("a kind with secret fields cannot use engine TTL (its keychain values would be orphaned)");
|
|
3671
|
+
}
|
|
3672
|
+
if ("maxRows" in retention || "maxRowsPerDevice" in retention) {
|
|
3673
|
+
if (retention.maxRowsPerDevice !== void 0 && decl.device !== "required") refuse("maxRowsPerDevice needs device: \"required\"");
|
|
3674
|
+
if (decl.at !== "required") refuse("a row bound trims the oldest \"at\": it needs at: \"required\"");
|
|
3675
|
+
if (hasSecrets) refuse("a kind with secret fields cannot use an engine row bound");
|
|
3676
|
+
const bound = Math.max(retention.maxRows ?? 0, retention.maxRowsPerDevice ?? 0);
|
|
3677
|
+
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)`);
|
|
3678
|
+
}
|
|
3679
|
+
if (decl.deviceRemoval === "delete" && decl.device === "none") refuse("deviceRemoval: \"delete\" needs a device: \"required\" or \"optional\"");
|
|
3680
|
+
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`);
|
|
3681
|
+
}
|
|
3682
|
+
function bodyFieldsOf(schema) {
|
|
3683
|
+
return { ...z.toJSONSchema(schema, {
|
|
3684
|
+
unrepresentable: "any",
|
|
3685
|
+
io: "input"
|
|
3686
|
+
}) };
|
|
3687
|
+
}
|
|
3688
|
+
function isRecordLike(value) {
|
|
3689
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3690
|
+
}
|
|
3691
|
+
/** Does every branch of the body schema carry `field`? (A discriminated union has several.) */
|
|
3692
|
+
function hasNameField(json, field) {
|
|
3693
|
+
const branches = json["oneOf"] ?? json["anyOf"];
|
|
3694
|
+
if (Array.isArray(branches)) return branches.length > 0 && branches.every((b) => isRecordLike(b) && hasNameField(b, field));
|
|
3695
|
+
const properties = json["properties"];
|
|
3696
|
+
return isRecordLike(properties) && Object.prototype.hasOwnProperty.call(properties, field);
|
|
3697
|
+
}
|
|
3698
|
+
/**
|
|
3699
|
+
* Declare the kinds of a package. The keys are the kind names. The result is
|
|
3700
|
+
* what `ctx.records.declare` sends to the engine and what `ctx.records.kind`
|
|
3701
|
+
* types its handle from.
|
|
3702
|
+
*/
|
|
3703
|
+
function defineRecordKinds(specs) {
|
|
3704
|
+
const out = {};
|
|
3705
|
+
for (const [name, spec] of Object.entries(specs)) {
|
|
3706
|
+
const secretPaths = collectZodSecretPaths(spec.schema);
|
|
3707
|
+
const nameField = spec.entry?.nameField ?? "name";
|
|
3708
|
+
const fields = bodyFieldsOf(spec.schema);
|
|
3709
|
+
if (spec.entry !== void 0 && !hasNameField(fields, nameField)) throw new DeclarationRefusedError(`kind "${name}": an entry kind's schema needs a "${nameField}" field`);
|
|
3710
|
+
const parsed = RecordKindDeclarationSchema.parse({
|
|
3711
|
+
database: spec.database,
|
|
3712
|
+
class: spec.class,
|
|
3713
|
+
writeTrigger: spec.writeTrigger,
|
|
3714
|
+
device: spec.device,
|
|
3715
|
+
at: spec.at,
|
|
3716
|
+
indexes: spec.indexes === void 0 ? void 0 : [...spec.indexes],
|
|
3717
|
+
retention: spec.retention,
|
|
3718
|
+
deviceRemoval: spec.deviceRemoval,
|
|
3719
|
+
maxDataBytes: spec.maxDataBytes,
|
|
3720
|
+
secretPaths: [...secretPaths],
|
|
3721
|
+
secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
|
|
3722
|
+
adoptsFrom: spec.adoptsFrom,
|
|
3723
|
+
transactFamily: spec.transactFamily,
|
|
3724
|
+
entry: spec.entry === void 0 ? void 0 : {
|
|
3725
|
+
title: spec.entry.title,
|
|
3726
|
+
nameField,
|
|
3727
|
+
fields
|
|
3728
|
+
}
|
|
3729
|
+
});
|
|
3730
|
+
assertValidDeclaration(name, parsed);
|
|
3731
|
+
out[name] = {
|
|
3732
|
+
name,
|
|
3733
|
+
schema: spec.schema,
|
|
3734
|
+
declaration: parsed
|
|
3735
|
+
};
|
|
3736
|
+
}
|
|
3737
|
+
return out;
|
|
3738
|
+
}
|
|
3739
|
+
/** The wire form of a package's kinds: name -> declaration. */
|
|
3740
|
+
function declarationsOf(kinds) {
|
|
3741
|
+
return Object.fromEntries(Object.entries(kinds).map(([name, kind]) => [name, kind.declaration]));
|
|
3742
|
+
}
|
|
3743
|
+
//#endregion
|
|
3744
|
+
//#region src/records/records-access.ts
|
|
3745
|
+
function isRecord$1(value) {
|
|
3746
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3747
|
+
}
|
|
3748
|
+
/** Run a transport call and give a wire failure its typed class back. */
|
|
3749
|
+
async function call(fn) {
|
|
3750
|
+
try {
|
|
3751
|
+
return await fn();
|
|
3752
|
+
} catch (err) {
|
|
3753
|
+
throw asRecordsError(err);
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
function zodSummary(error) {
|
|
3757
|
+
return error.issues.slice(0, 5).map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
3758
|
+
}
|
|
3759
|
+
function toRow(view, data) {
|
|
3760
|
+
return {
|
|
3761
|
+
id: view.id,
|
|
3762
|
+
deviceId: view.deviceId,
|
|
3763
|
+
at: view.at,
|
|
3764
|
+
expiresAt: view.expiresAt,
|
|
3765
|
+
rev: view.rev,
|
|
3766
|
+
updatedAt: view.updatedAt,
|
|
3767
|
+
data
|
|
3768
|
+
};
|
|
3769
|
+
}
|
|
3770
|
+
var KindHandle = class {
|
|
3771
|
+
def;
|
|
3772
|
+
o;
|
|
3773
|
+
name;
|
|
3774
|
+
decl;
|
|
3775
|
+
constructor(def, o) {
|
|
3776
|
+
this.def = def;
|
|
3777
|
+
this.o = o;
|
|
3778
|
+
this.name = def.name;
|
|
3779
|
+
this.decl = def.declaration;
|
|
3780
|
+
}
|
|
3781
|
+
get hasSecrets() {
|
|
3782
|
+
return this.decl.secretPaths.length > 0;
|
|
3783
|
+
}
|
|
3784
|
+
requireSecrets() {
|
|
3785
|
+
if (this.o.secrets === null) throw new RecordSecretUnavailableError(`kind "${this.name}" has secret fields and this process has no keychain door`);
|
|
3786
|
+
return this.o.secrets;
|
|
3787
|
+
}
|
|
3788
|
+
/** Read the body as the schema's type. The engine already redacted the secrets. */
|
|
3789
|
+
typed(view) {
|
|
3790
|
+
return toRow(view, view.data);
|
|
3791
|
+
}
|
|
3792
|
+
/**
|
|
3793
|
+
* Classify and store the secret leaves of a body about to be written. The
|
|
3794
|
+
* keychain is written FIRST (a failure throws before any row is touched), and
|
|
3795
|
+
* what each ref held before is remembered: when the row write is then refused
|
|
3796
|
+
* (a stale `ifRev`, too large, a unique index) `rollback` puts the previous
|
|
3797
|
+
* values back, so a refused write never destroys the secret that was stored.
|
|
3798
|
+
*/
|
|
3799
|
+
async storeSecrets(id, body, mode) {
|
|
3800
|
+
if (!this.hasSecrets) return {
|
|
3801
|
+
body,
|
|
3802
|
+
rollback: async () => {}
|
|
3803
|
+
};
|
|
3804
|
+
const secrets = this.requireSecrets();
|
|
3805
|
+
const writes = [];
|
|
3806
|
+
const prepared = mapSecretLeaves(body, this.decl.secretPaths, this.decl.secretArrayKeys, (leaf) => {
|
|
3807
|
+
const intent = classifySecretWrite(leaf.value);
|
|
3808
|
+
if (intent.kind === "clear") return REMOVE_LEAF;
|
|
3809
|
+
if (intent.kind === "keep") {
|
|
3810
|
+
if (mode === "put") return REDACTED_SECRET;
|
|
3811
|
+
if (leaf.value === null) return null;
|
|
3812
|
+
return leaf.concrete.includes("[") ? REDACTED_SECRET : REMOVE_LEAF;
|
|
3813
|
+
}
|
|
3814
|
+
const ref = recordSecretRef(this.o.owner, this.name, id, leaf.concrete);
|
|
3815
|
+
writes.push({
|
|
3816
|
+
ref,
|
|
3817
|
+
value: intent.value
|
|
3818
|
+
});
|
|
3819
|
+
return makeSecretMarker(ref);
|
|
3820
|
+
});
|
|
3821
|
+
const done = [];
|
|
3822
|
+
const rollback = async () => {
|
|
3823
|
+
for (const { ref, previous } of done) try {
|
|
3824
|
+
if (previous === void 0) await secrets.delete([ref]);
|
|
3825
|
+
else await secrets.put(ref, previous);
|
|
3826
|
+
} catch (err) {
|
|
3827
|
+
this.o.logger?.warn("records: could not restore a keychain value after a refused write", { meta: {
|
|
3828
|
+
kind: this.name,
|
|
3829
|
+
error: err instanceof Error ? err.message : "unknown"
|
|
3830
|
+
} });
|
|
3831
|
+
}
|
|
3832
|
+
};
|
|
3833
|
+
for (const write of writes) try {
|
|
3834
|
+
const before = (await secrets.resolve([write.ref]))[write.ref];
|
|
3835
|
+
const previous = before?.kind === "value" ? before.value : void 0;
|
|
3836
|
+
await secrets.put(write.ref, write.value);
|
|
3837
|
+
done.push({
|
|
3838
|
+
ref: write.ref,
|
|
3839
|
+
previous
|
|
3840
|
+
});
|
|
3841
|
+
} catch (err) {
|
|
3842
|
+
await rollback();
|
|
3843
|
+
throw new RecordSecretUnavailableError(`cannot store a secret of ${this.name}/${id}: ${err instanceof Error ? err.message : "the keychain refused"}. Unlock the keychain (System -> Secrets) and save again`);
|
|
3844
|
+
}
|
|
3845
|
+
return {
|
|
3846
|
+
body: prepared,
|
|
3847
|
+
rollback
|
|
3848
|
+
};
|
|
3849
|
+
}
|
|
3850
|
+
async releaseRefs(refs) {
|
|
3851
|
+
if (refs.length === 0 || this.o.secrets === null) return;
|
|
3852
|
+
try {
|
|
3853
|
+
await this.o.secrets.delete(refs);
|
|
3854
|
+
} catch (err) {
|
|
3855
|
+
this.o.logger?.warn("records: could not remove released keychain values", { meta: {
|
|
3856
|
+
kind: this.name,
|
|
3857
|
+
count: refs.length,
|
|
3858
|
+
error: err instanceof Error ? err.message : "unknown"
|
|
3859
|
+
} });
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
validate(data, id) {
|
|
3863
|
+
const shaped = this.hasSecrets ? mapSecretLeaves(data, this.decl.secretPaths, this.decl.secretArrayKeys, (leaf) => {
|
|
3864
|
+
const intent = classifySecretWrite(leaf.value);
|
|
3865
|
+
if (intent.kind === "clear") return REMOVE_LEAF;
|
|
3866
|
+
return intent.kind === "keep" ? REDACTED_SECRET : leaf.value;
|
|
3867
|
+
}) : data;
|
|
3868
|
+
const parsed = this.def.schema.safeParse(shaped);
|
|
3869
|
+
if (!parsed.success) throw new InvalidRecordError(`${this.name}/${id}: ${zodSummary(parsed.error)}`);
|
|
3870
|
+
if (!isRecord$1(parsed.data)) throw new InvalidRecordError(`${this.name}/${id}: the body must be an object`);
|
|
3871
|
+
return parsed.data;
|
|
3872
|
+
}
|
|
3873
|
+
async get(id) {
|
|
3874
|
+
const view = await call(() => this.o.transport.get({
|
|
3875
|
+
kind: this.name,
|
|
3876
|
+
id
|
|
3877
|
+
}));
|
|
3878
|
+
return view === null ? null : this.typed(view);
|
|
3879
|
+
}
|
|
3880
|
+
async getMany(ids) {
|
|
3881
|
+
if (ids.length === 0) return [];
|
|
3882
|
+
return (await call(() => this.o.transport.getMany({
|
|
3883
|
+
kind: this.name,
|
|
3884
|
+
ids: [...ids]
|
|
3885
|
+
}))).map((v) => this.typed(v));
|
|
3886
|
+
}
|
|
3887
|
+
async getResolved(id) {
|
|
3888
|
+
const row = await this.get(id);
|
|
3889
|
+
if (row === null) return null;
|
|
3890
|
+
return (await this.resolveRows([row]))[0] ?? row;
|
|
3891
|
+
}
|
|
3892
|
+
async resolveRows(rows) {
|
|
3893
|
+
if (!this.hasSecrets || rows.length === 0) return rows;
|
|
3894
|
+
const secrets = this.requireSecrets();
|
|
3895
|
+
const refsById = await call(() => this.o.transport.secretRefs({
|
|
3896
|
+
kind: this.name,
|
|
3897
|
+
ids: rows.map((r) => r.id)
|
|
3898
|
+
}));
|
|
3899
|
+
const refs = Object.values(refsById).flatMap((byPath) => Object.values(byPath));
|
|
3900
|
+
if (refs.length === 0) return rows;
|
|
3901
|
+
const answers = await secrets.resolve(refs);
|
|
3902
|
+
return rows.map((row) => {
|
|
3903
|
+
const byPath = refsById[row.id] ?? {};
|
|
3904
|
+
const resolved = mapSecretLeaves(row.data, this.decl.secretPaths, this.decl.secretArrayKeys, (leaf) => {
|
|
3905
|
+
const ref = byPath[leaf.concrete];
|
|
3906
|
+
if (ref === void 0) return leaf.value;
|
|
3907
|
+
const answer = answers[ref];
|
|
3908
|
+
if (answer?.kind === "value") return answer.value;
|
|
3909
|
+
const why = answer?.kind === "unavailable" ? answer.reason : answer?.kind ?? "no answer";
|
|
3910
|
+
throw new RecordSecretUnavailableError(`the secret "${leaf.concrete}" of ${this.name}/${row.id} cannot be served (${why})`);
|
|
3911
|
+
});
|
|
3912
|
+
return {
|
|
3913
|
+
...row,
|
|
3914
|
+
data: resolved
|
|
3915
|
+
};
|
|
3916
|
+
});
|
|
3917
|
+
}
|
|
3918
|
+
async put(input, opts = {}) {
|
|
3919
|
+
const checked = this.validate(input.data, input.id);
|
|
3920
|
+
const { body, rollback } = await this.storeSecrets(input.id, checked, "put");
|
|
3921
|
+
const result = await call(() => this.o.transport.put({
|
|
3922
|
+
kind: this.name,
|
|
3923
|
+
record: {
|
|
3924
|
+
...input,
|
|
3925
|
+
data: toBody(body)
|
|
3926
|
+
},
|
|
3927
|
+
...opts.ifRev === void 0 ? {} : { ifRev: opts.ifRev }
|
|
3928
|
+
})).catch(async (err) => {
|
|
3929
|
+
await rollback();
|
|
3930
|
+
throw err;
|
|
3931
|
+
});
|
|
3932
|
+
await this.releaseRefs(result.releasedRefs);
|
|
3933
|
+
return { rev: result.rev };
|
|
3934
|
+
}
|
|
3935
|
+
async putMany(inputs) {
|
|
3936
|
+
if (inputs.length === 0) return { count: 0 };
|
|
3937
|
+
const prepared = [];
|
|
3938
|
+
const rollbacks = [];
|
|
3939
|
+
try {
|
|
3940
|
+
for (const input of inputs) {
|
|
3941
|
+
const checked = this.validate(input.data, input.id);
|
|
3942
|
+
const { body, rollback } = await this.storeSecrets(input.id, checked, "put");
|
|
3943
|
+
rollbacks.push(rollback);
|
|
3944
|
+
prepared.push({
|
|
3945
|
+
...input,
|
|
3946
|
+
data: toBody(body)
|
|
3947
|
+
});
|
|
3948
|
+
}
|
|
3949
|
+
} catch (err) {
|
|
3950
|
+
for (const rollback of rollbacks) await rollback();
|
|
3951
|
+
throw err;
|
|
3952
|
+
}
|
|
3953
|
+
const result = await call(() => this.o.transport.putMany({
|
|
3954
|
+
kind: this.name,
|
|
3955
|
+
records: prepared
|
|
3956
|
+
})).catch(async (err) => {
|
|
3957
|
+
for (const rollback of rollbacks) await rollback();
|
|
3958
|
+
throw err;
|
|
3959
|
+
});
|
|
3960
|
+
await this.releaseRefs(result.releasedRefs);
|
|
3961
|
+
return { count: result.count };
|
|
3962
|
+
}
|
|
3963
|
+
async patch(id, partial, opts = {}) {
|
|
3964
|
+
const { body, rollback } = await this.storeSecrets(id, partial, "patch");
|
|
3965
|
+
const result = await call(() => this.o.transport.patch({
|
|
3966
|
+
kind: this.name,
|
|
3967
|
+
id,
|
|
3968
|
+
data: toBody(body),
|
|
3969
|
+
...opts.at === void 0 ? {} : { at: opts.at },
|
|
3970
|
+
...opts.expiresAt === void 0 ? {} : { expiresAt: opts.expiresAt },
|
|
3971
|
+
...opts.ifRev === void 0 ? {} : { ifRev: opts.ifRev }
|
|
3972
|
+
})).catch(async (err) => {
|
|
3973
|
+
await rollback();
|
|
3974
|
+
throw err;
|
|
3975
|
+
});
|
|
3976
|
+
await this.releaseRefs(result.releasedRefs);
|
|
3977
|
+
return { rev: result.rev };
|
|
3978
|
+
}
|
|
3979
|
+
async delete(id) {
|
|
3980
|
+
const result = await call(() => this.o.transport.delete({
|
|
3981
|
+
kind: this.name,
|
|
3982
|
+
id
|
|
3983
|
+
}));
|
|
3984
|
+
await this.releaseRefs(result.secretRefs);
|
|
3985
|
+
return result.deleted;
|
|
3986
|
+
}
|
|
3987
|
+
async deleteMany(ids) {
|
|
3988
|
+
if (ids.length === 0) return 0;
|
|
3989
|
+
const result = await call(() => this.o.transport.deleteMany({
|
|
3990
|
+
kind: this.name,
|
|
3991
|
+
ids: [...ids]
|
|
3992
|
+
}));
|
|
3993
|
+
await this.releaseRefs(result.secretRefs);
|
|
3994
|
+
return result.deleted;
|
|
3995
|
+
}
|
|
3996
|
+
async query(q, opts = {}) {
|
|
3997
|
+
const result = await call(() => this.o.transport.query({
|
|
3998
|
+
...q,
|
|
3999
|
+
kind: this.name,
|
|
4000
|
+
limit: q.limit ?? 100
|
|
4001
|
+
}));
|
|
4002
|
+
const rows = result.records.map((v) => this.typed(v));
|
|
4003
|
+
return {
|
|
4004
|
+
records: opts.resolveSecrets === true ? await this.resolveRows(rows) : rows,
|
|
4005
|
+
nextCursor: result.nextCursor
|
|
4006
|
+
};
|
|
4007
|
+
}
|
|
4008
|
+
async count(q) {
|
|
4009
|
+
return (await call(() => this.o.transport.count({
|
|
4010
|
+
...q,
|
|
4011
|
+
kind: this.name
|
|
4012
|
+
}))).count;
|
|
4013
|
+
}
|
|
4014
|
+
async histogram(q) {
|
|
4015
|
+
return (await call(() => this.o.transport.histogram({
|
|
4016
|
+
...q,
|
|
4017
|
+
kind: this.name
|
|
4018
|
+
}))).buckets;
|
|
4019
|
+
}
|
|
4020
|
+
async aggregate(q) {
|
|
4021
|
+
return (await call(() => this.o.transport.aggregate({
|
|
4022
|
+
...q,
|
|
4023
|
+
kind: this.name
|
|
4024
|
+
}))).rows;
|
|
4025
|
+
}
|
|
4026
|
+
async deleteWhere(q) {
|
|
4027
|
+
const result = await call(() => this.o.transport.deleteWhere({
|
|
4028
|
+
...q,
|
|
4029
|
+
kind: this.name
|
|
4030
|
+
}));
|
|
4031
|
+
await this.releaseRefs(result.secretRefs);
|
|
4032
|
+
return result.deleted;
|
|
4033
|
+
}
|
|
4034
|
+
async allocateId() {
|
|
4035
|
+
return (await call(() => this.o.transport.allocateId({ kind: this.name }))).id;
|
|
4036
|
+
}
|
|
4037
|
+
};
|
|
4038
|
+
function toBody(value) {
|
|
4039
|
+
if (!isRecord$1(value)) throw new InvalidRecordError("the record body must be an object");
|
|
4040
|
+
return value;
|
|
4041
|
+
}
|
|
4042
|
+
/** Build the owner's records handle. */
|
|
4043
|
+
function createRecordsAccess(options) {
|
|
4044
|
+
const handles = /* @__PURE__ */ new WeakMap();
|
|
4045
|
+
const delegates = /* @__PURE__ */ new Map();
|
|
4046
|
+
return {
|
|
4047
|
+
owner: options.owner,
|
|
4048
|
+
forOwner: options.transportForOwner === void 0 ? null : (other) => {
|
|
4049
|
+
const existing = delegates.get(other);
|
|
4050
|
+
if (existing !== void 0) return existing;
|
|
4051
|
+
const build = options.transportForOwner;
|
|
4052
|
+
if (build === void 0) throw new Error("forOwner: no delegate transport");
|
|
4053
|
+
const delegate = createRecordsAccess({
|
|
4054
|
+
owner: other,
|
|
4055
|
+
transport: build(other),
|
|
4056
|
+
secrets: options.secrets?.forOwner?.(other) ?? null,
|
|
4057
|
+
...options.logger === void 0 ? {} : { logger: options.logger }
|
|
4058
|
+
});
|
|
4059
|
+
delegates.set(other, delegate);
|
|
4060
|
+
return delegate;
|
|
4061
|
+
},
|
|
4062
|
+
async declare(kinds) {
|
|
4063
|
+
const result = await call(() => options.transport.declareKinds({ kinds: { ...declarationsOf(kinds) } }));
|
|
4064
|
+
const pending = Object.entries(result.kinds).filter((entry) => entry[1].adoptionPending !== null);
|
|
4065
|
+
if (pending.length > 0) throw new AdoptionPendingError(`${pending.map(([name, r]) => r.adoptionPending === null ? name : `${name} (legacy table "${r.adoptionPending.legacyTable}" holds ${r.adoptionPending.legacyRows} rows)`).join(", ")}. Stop the addon and run scripts/hub/records-adopt.ts --copy --apply before starting this build`);
|
|
4066
|
+
return result.kinds;
|
|
4067
|
+
},
|
|
4068
|
+
kind(definition) {
|
|
4069
|
+
const existing = handles.get(definition);
|
|
4070
|
+
if (existing !== void 0) return existing;
|
|
4071
|
+
const handle = new KindHandle(definition, options);
|
|
4072
|
+
handles.set(definition, handle);
|
|
4073
|
+
return handle;
|
|
4074
|
+
},
|
|
4075
|
+
async deleteDevice(deviceId, opts = {}) {
|
|
4076
|
+
const result = await call(() => options.transport.deleteDevice({
|
|
4077
|
+
deviceId,
|
|
4078
|
+
...opts.kinds === void 0 ? {} : { kinds: [...opts.kinds] }
|
|
4079
|
+
}));
|
|
4080
|
+
if (result.secretRefs.length > 0 && options.secrets !== null) try {
|
|
4081
|
+
await options.secrets.delete(result.secretRefs);
|
|
4082
|
+
} catch (err) {
|
|
4083
|
+
options.logger?.warn("records: could not remove released keychain values", { meta: {
|
|
4084
|
+
count: result.secretRefs.length,
|
|
4085
|
+
error: err instanceof Error ? err.message : "unknown"
|
|
4086
|
+
} });
|
|
4087
|
+
}
|
|
4088
|
+
return result.deleted;
|
|
4089
|
+
}
|
|
4090
|
+
};
|
|
4091
|
+
}
|
|
4092
|
+
//#endregion
|
|
4093
|
+
//#region src/records/entry-kind.ts
|
|
4094
|
+
/**
|
|
4095
|
+
* Entry kinds (D881): every list of user-created entries (notification
|
|
4096
|
+
* targets, brokers, LLM profiles) is ONE record kind with an id, a name and a
|
|
4097
|
+
* kind-specific body, and the admin UI lists/adds/edits/deletes all of them
|
|
4098
|
+
* through the `record-entries` capability with password fields derived from
|
|
4099
|
+
* the schema (`.meta({ secret: true })`).
|
|
4100
|
+
*/
|
|
4101
|
+
/** The body of a plain entry: a name and the fields the kind adds. */
|
|
4102
|
+
function entryBody(shape) {
|
|
4103
|
+
return z.object({
|
|
4104
|
+
name: z.string().min(1),
|
|
4105
|
+
...shape
|
|
4106
|
+
});
|
|
4107
|
+
}
|
|
4108
|
+
/**
|
|
4109
|
+
* Declare one entry kind. An entry is configuration: never trimmed by the
|
|
4110
|
+
* engine, never aged, belonging to no device.
|
|
4111
|
+
*/
|
|
4112
|
+
function defineEntryKind(name, spec) {
|
|
4113
|
+
const defined = defineRecordKinds({ [name]: {
|
|
4114
|
+
schema: spec.schema,
|
|
4115
|
+
class: "config",
|
|
4116
|
+
writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
|
|
4117
|
+
retention: { none: "an entry goes only when the operator deletes it" },
|
|
4118
|
+
...spec.database === void 0 ? {} : { database: spec.database },
|
|
4119
|
+
...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
|
|
4120
|
+
...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
|
|
4121
|
+
...spec.adoptsFrom === void 0 ? {} : { adoptsFrom: spec.adoptsFrom },
|
|
4122
|
+
entry: {
|
|
4123
|
+
title: spec.title,
|
|
4124
|
+
...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
|
|
4125
|
+
}
|
|
4126
|
+
} })[name];
|
|
4127
|
+
if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
|
|
4128
|
+
return defined;
|
|
4129
|
+
}
|
|
4130
|
+
//#endregion
|
|
4131
|
+
//#region src/records/settings-brokers-adopts-from.ts
|
|
4132
|
+
/**
|
|
4133
|
+
* Legacy source for broker entry adoption (B2): brokers live in addon-settings
|
|
4134
|
+
* as one row `<addonId>.brokers` whose value is an array until records-adopt
|
|
4135
|
+
* copies them into the entry kind.
|
|
4136
|
+
*/
|
|
4137
|
+
var SETTINGS_BROKERS_ADOPTS_PREFIX = "@settings-brokers/";
|
|
4138
|
+
/** The `adoptsFrom` value for an addon's broker entry kind. */
|
|
4139
|
+
function settingsBrokersAdoptsFrom(addonId) {
|
|
4140
|
+
return `${SETTINGS_BROKERS_ADOPTS_PREFIX}${addonId}`;
|
|
4141
|
+
}
|
|
4142
|
+
/** When non-null, `from` names an addon-settings brokers blob, not a SQL table. */
|
|
4143
|
+
function parseSettingsBrokersAdoptsFrom(from) {
|
|
4144
|
+
if (!from.startsWith("@settings-brokers/")) return null;
|
|
4145
|
+
const addonId = from.slice(18);
|
|
4146
|
+
return addonId.length > 0 ? addonId : null;
|
|
4147
|
+
}
|
|
4148
|
+
//#endregion
|
|
4149
|
+
//#region src/records/settings-array-adopts-from.ts
|
|
4150
|
+
/**
|
|
4151
|
+
* Legacy source for entry adoption (B3): a list stored under addon-settings
|
|
4152
|
+
* `<addonId>.<key>` until records-adopt copies rows into the entry kind.
|
|
4153
|
+
*/
|
|
4154
|
+
var SETTINGS_ARRAY_ADOPTS_PREFIX = "@settings-array/";
|
|
4155
|
+
/** The `adoptsFrom` value for an addon-settings array field. */
|
|
4156
|
+
function settingsArrayAdoptsFrom(addonId, settingsKey) {
|
|
4157
|
+
return `${SETTINGS_ARRAY_ADOPTS_PREFIX}${addonId}/${settingsKey}`;
|
|
4158
|
+
}
|
|
4159
|
+
/** When non-null, `from` names an addon-settings array blob, not a SQL table. */
|
|
4160
|
+
function parseSettingsArrayAdoptsFrom(from) {
|
|
4161
|
+
if (!from.startsWith("@settings-array/")) return null;
|
|
4162
|
+
const rest = from.slice(16);
|
|
4163
|
+
const slash = rest.indexOf("/");
|
|
4164
|
+
if (slash <= 0 || slash >= rest.length - 1) return null;
|
|
4165
|
+
const addonId = rest.slice(0, slash);
|
|
4166
|
+
const settingsKey = rest.slice(slash + 1);
|
|
4167
|
+
if (addonId.length === 0 || settingsKey.length === 0) return null;
|
|
4168
|
+
return {
|
|
4169
|
+
addonId,
|
|
4170
|
+
settingsKey
|
|
4171
|
+
};
|
|
4172
|
+
}
|
|
4173
|
+
//#endregion
|
|
4174
|
+
//#region src/records/in-memory-records.ts
|
|
4175
|
+
/**
|
|
4176
|
+
* In-memory `RecordsAccess` for specs. Kinds are declared by the caller.
|
|
4177
|
+
* Lives in `@camstack/types` so a spec does not import another package's source.
|
|
4178
|
+
*/
|
|
4179
|
+
function isRecord(value) {
|
|
4180
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4181
|
+
}
|
|
4182
|
+
function markersOf(decl, body) {
|
|
4183
|
+
const out = /* @__PURE__ */ new Map();
|
|
4184
|
+
mapSecretLeaves(body, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
|
|
4185
|
+
if (isSecretMarker(leaf.value)) out.set(leaf.concrete, leaf.value.$secret);
|
|
4186
|
+
return leaf.value;
|
|
4187
|
+
});
|
|
4188
|
+
return out;
|
|
4189
|
+
}
|
|
4190
|
+
function makeSpecKeychain(owner) {
|
|
4191
|
+
const values = /* @__PURE__ */ new Map();
|
|
4192
|
+
return { access: {
|
|
4193
|
+
owner,
|
|
4194
|
+
ref: (scope, fieldPath) => buildSecretRef({
|
|
4195
|
+
owner,
|
|
4196
|
+
scope,
|
|
4197
|
+
fieldPath
|
|
4198
|
+
}),
|
|
4199
|
+
resolve: async (refs) => {
|
|
4200
|
+
const out = {};
|
|
4201
|
+
for (const ref of refs) {
|
|
4202
|
+
const value = values.get(ref);
|
|
4203
|
+
out[ref] = value === void 0 ? { kind: "missing" } : {
|
|
4204
|
+
kind: "value",
|
|
4205
|
+
value,
|
|
4206
|
+
version: 1
|
|
4207
|
+
};
|
|
4208
|
+
}
|
|
4209
|
+
return out;
|
|
4210
|
+
},
|
|
4211
|
+
put: async (ref, value) => {
|
|
4212
|
+
values.set(ref, value);
|
|
4213
|
+
return 1;
|
|
4214
|
+
},
|
|
4215
|
+
delete: async (refs) => {
|
|
4216
|
+
let deleted = 0;
|
|
4217
|
+
for (const ref of refs) if (values.delete(ref)) deleted += 1;
|
|
4218
|
+
return deleted;
|
|
4219
|
+
},
|
|
4220
|
+
state: async () => ({ state: "ready" }),
|
|
4221
|
+
onChange: () => () => void 0,
|
|
4222
|
+
forOwner: null
|
|
4223
|
+
} };
|
|
4224
|
+
}
|
|
4225
|
+
function makeInMemoryRecords(owner = "spec") {
|
|
4226
|
+
const decls = /* @__PURE__ */ new Map();
|
|
4227
|
+
const tables = /* @__PURE__ */ new Map();
|
|
4228
|
+
let clock = 1;
|
|
4229
|
+
let writesFail = false;
|
|
4230
|
+
let readsFail = false;
|
|
4231
|
+
const table = (kind) => {
|
|
4232
|
+
let t = tables.get(kind);
|
|
4233
|
+
if (t === void 0) {
|
|
4234
|
+
t = /* @__PURE__ */ new Map();
|
|
4235
|
+
tables.set(kind, t);
|
|
4236
|
+
}
|
|
4237
|
+
return t;
|
|
4238
|
+
};
|
|
4239
|
+
const declOf = (kind) => {
|
|
4240
|
+
const decl = decls.get(kind);
|
|
4241
|
+
if (decl === void 0) throw new Error(`kind "${kind}" is not declared`);
|
|
4242
|
+
return decl;
|
|
4243
|
+
};
|
|
4244
|
+
const redact = (decl, body) => {
|
|
4245
|
+
if (decl.secretPaths.length === 0) return body;
|
|
4246
|
+
const out = mapSecretLeaves(body, decl.secretPaths, decl.secretArrayKeys, (leaf) => leaf.value === null ? null : REDACTED_SECRET);
|
|
4247
|
+
return isRecord(out) ? out : body;
|
|
4248
|
+
};
|
|
4249
|
+
const view = (kind, row) => ({
|
|
4250
|
+
id: row.id,
|
|
4251
|
+
deviceId: null,
|
|
4252
|
+
at: row.at,
|
|
4253
|
+
expiresAt: null,
|
|
4254
|
+
rev: row.rev,
|
|
4255
|
+
updatedAt: row.updatedAt,
|
|
4256
|
+
data: redact(declOf(kind), row.data)
|
|
4257
|
+
});
|
|
4258
|
+
const enforce = (kind, id, body, stored) => {
|
|
4259
|
+
const decl = declOf(kind);
|
|
4260
|
+
if (decl.secretPaths.length === 0) return {
|
|
4261
|
+
body,
|
|
4262
|
+
released: []
|
|
4263
|
+
};
|
|
4264
|
+
const before = stored === null ? /* @__PURE__ */ new Map() : markersOf(decl, stored);
|
|
4265
|
+
const next = mapSecretLeaves(body, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
|
|
4266
|
+
const expected = recordSecretRef(owner, kind, id, leaf.concrete);
|
|
4267
|
+
if (isSecretMarker(leaf.value)) {
|
|
4268
|
+
if (leaf.value.$secret !== expected) throw new Error("foreign secret marker");
|
|
4269
|
+
return leaf.value;
|
|
4270
|
+
}
|
|
4271
|
+
if (leaf.value === "__camstack_redacted__") {
|
|
4272
|
+
const keep = before.get(leaf.concrete);
|
|
4273
|
+
return keep === void 0 ? REMOVE_LEAF : {
|
|
4274
|
+
$secret: keep,
|
|
4275
|
+
v: 1
|
|
4276
|
+
};
|
|
4277
|
+
}
|
|
4278
|
+
if (leaf.value === null || leaf.value === void 0) return REMOVE_LEAF;
|
|
4279
|
+
throw new Error(`${kind}/${id}: a secret field takes a keychain marker, never a value`);
|
|
4280
|
+
});
|
|
4281
|
+
if (!isRecord(next)) throw new Error("the body must be an object");
|
|
4282
|
+
const after = markersOf(decl, next);
|
|
4283
|
+
return {
|
|
4284
|
+
body: next,
|
|
4285
|
+
released: [...before.entries()].filter(([p, ref]) => after.get(p) !== ref).map(([, r]) => r)
|
|
4286
|
+
};
|
|
4287
|
+
};
|
|
4288
|
+
const write = (kind, input) => {
|
|
4289
|
+
if (writesFail) throw new Error("records engine unreachable");
|
|
4290
|
+
if (declOf(kind).at === "required" && input.at === void 0) throw new Error(`${kind}/${input.id}: the kind needs "at" on every write`);
|
|
4291
|
+
const prev = table(kind).get(input.id);
|
|
4292
|
+
const { body, released } = enforce(kind, input.id, input.data, prev?.data ?? null);
|
|
4293
|
+
const rev = (prev?.rev ?? 0) + 1;
|
|
4294
|
+
clock += 1;
|
|
4295
|
+
table(kind).set(input.id, {
|
|
4296
|
+
id: input.id,
|
|
4297
|
+
at: input.at ?? null,
|
|
4298
|
+
rev,
|
|
4299
|
+
updatedAt: clock,
|
|
4300
|
+
data: body
|
|
4301
|
+
});
|
|
4302
|
+
return {
|
|
4303
|
+
rev,
|
|
4304
|
+
released
|
|
4305
|
+
};
|
|
4306
|
+
};
|
|
4307
|
+
const unimplemented = (op) => {
|
|
4308
|
+
throw new Error(`the fake records transport does not implement ${op}`);
|
|
4309
|
+
};
|
|
4310
|
+
return {
|
|
4311
|
+
records: createRecordsAccess({
|
|
4312
|
+
owner,
|
|
4313
|
+
transport: {
|
|
4314
|
+
declareKinds: async ({ kinds }) => {
|
|
4315
|
+
const out = {};
|
|
4316
|
+
let n = 0;
|
|
4317
|
+
for (const [name, decl] of Object.entries(kinds)) {
|
|
4318
|
+
decls.set(name, decl);
|
|
4319
|
+
n += 1;
|
|
4320
|
+
out[name] = {
|
|
4321
|
+
kindId: n,
|
|
4322
|
+
state: "created",
|
|
4323
|
+
createdIndexes: decl.indexes.map((i) => i.name),
|
|
4324
|
+
adoptionPending: null
|
|
4325
|
+
};
|
|
4326
|
+
}
|
|
4327
|
+
return { kinds: out };
|
|
4328
|
+
},
|
|
4329
|
+
get: async ({ kind, id }) => {
|
|
4330
|
+
if (readsFail) throw new Error("records engine unreachable");
|
|
4331
|
+
const row = table(kind).get(id);
|
|
4332
|
+
return row === void 0 ? null : view(kind, row);
|
|
4333
|
+
},
|
|
4334
|
+
getMany: async ({ kind, ids }) => ids.flatMap((id) => {
|
|
4335
|
+
const row = table(kind).get(id);
|
|
4336
|
+
return row === void 0 ? [] : [view(kind, row)];
|
|
4337
|
+
}),
|
|
4338
|
+
secretRefs: async ({ kind, ids }) => {
|
|
4339
|
+
const decl = declOf(kind);
|
|
4340
|
+
const out = {};
|
|
4341
|
+
for (const id of ids) {
|
|
4342
|
+
const row = table(kind).get(id);
|
|
4343
|
+
if (row !== void 0) out[id] = Object.fromEntries(markersOf(decl, row.data));
|
|
4344
|
+
}
|
|
4345
|
+
return out;
|
|
4346
|
+
},
|
|
4347
|
+
put: async ({ kind, record, ifRev }) => {
|
|
4348
|
+
if (ifRev !== void 0 && (table(kind).get(record.id)?.rev ?? 0) !== ifRev) throw new Error("stale ifRev");
|
|
4349
|
+
const { rev, released } = write(kind, record);
|
|
4350
|
+
return {
|
|
4351
|
+
rev,
|
|
4352
|
+
releasedRefs: released
|
|
4353
|
+
};
|
|
4354
|
+
},
|
|
4355
|
+
putMany: async ({ kind, records }) => {
|
|
4356
|
+
const released = [];
|
|
4357
|
+
for (const record of records) released.push(...write(kind, record).released);
|
|
4358
|
+
return {
|
|
4359
|
+
count: records.length,
|
|
4360
|
+
releasedRefs: released
|
|
4361
|
+
};
|
|
4362
|
+
},
|
|
4363
|
+
patch: async () => unimplemented("patch"),
|
|
4364
|
+
delete: async ({ kind, id }) => {
|
|
4365
|
+
if (writesFail) throw new Error("records engine unreachable");
|
|
4366
|
+
const row = table(kind).get(id);
|
|
4367
|
+
if (row === void 0) return {
|
|
4368
|
+
deleted: false,
|
|
4369
|
+
secretRefs: []
|
|
4370
|
+
};
|
|
4371
|
+
table(kind).delete(id);
|
|
4372
|
+
return {
|
|
4373
|
+
deleted: true,
|
|
4374
|
+
secretRefs: [...markersOf(declOf(kind), row.data).values()]
|
|
4375
|
+
};
|
|
4376
|
+
},
|
|
4377
|
+
deleteMany: async ({ kind, ids }) => {
|
|
4378
|
+
let deleted = 0;
|
|
4379
|
+
const refs = [];
|
|
4380
|
+
for (const id of ids) {
|
|
4381
|
+
const row = table(kind).get(id);
|
|
4382
|
+
if (row === void 0) continue;
|
|
4383
|
+
table(kind).delete(id);
|
|
4384
|
+
deleted += 1;
|
|
4385
|
+
refs.push(...markersOf(declOf(kind), row.data).values());
|
|
4386
|
+
}
|
|
4387
|
+
return {
|
|
4388
|
+
deleted,
|
|
4389
|
+
secretRefs: refs
|
|
4390
|
+
};
|
|
4391
|
+
},
|
|
4392
|
+
query: async (q) => {
|
|
4393
|
+
if (readsFail) throw new Error("records engine unreachable");
|
|
4394
|
+
const decl = declOf(q.kind);
|
|
4395
|
+
const asked = Object.keys(q.where ?? {});
|
|
4396
|
+
if (asked.length > 0) {
|
|
4397
|
+
if (!decl.indexes.some((index) => {
|
|
4398
|
+
const leading = new Set(index.fields.slice(0, asked.length));
|
|
4399
|
+
return asked.length <= index.fields.length && asked.every((f) => leading.has(f));
|
|
4400
|
+
})) throw new Error(`${q.kind}: no declared index serves [${asked.join(", ")}]`);
|
|
4401
|
+
}
|
|
4402
|
+
let rows = [...table(q.kind).values()];
|
|
4403
|
+
for (const [field, wanted] of Object.entries(q.where ?? {})) rows = rows.filter((r) => r.data[field] === wanted);
|
|
4404
|
+
const byAt = q.order === "at" || q.order === void 0 && decl.at === "required";
|
|
4405
|
+
rows.sort((a, b) => byAt ? (a.at ?? 0) - (b.at ?? 0) : a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
4406
|
+
if (q.desc === true) rows.reverse();
|
|
4407
|
+
const start = q.cursor === void 0 ? 0 : Number(q.cursor);
|
|
4408
|
+
const page = rows.slice(start, start + q.limit);
|
|
4409
|
+
const more = start + q.limit < rows.length;
|
|
4410
|
+
return {
|
|
4411
|
+
records: page.map((r) => view(q.kind, r)),
|
|
4412
|
+
nextCursor: more ? String(start + q.limit) : null
|
|
4413
|
+
};
|
|
4414
|
+
},
|
|
4415
|
+
count: async ({ kind }) => ({ count: table(kind).size }),
|
|
4416
|
+
histogram: async () => unimplemented("histogram"),
|
|
4417
|
+
aggregate: async () => unimplemented("aggregate"),
|
|
4418
|
+
deleteWhere: async () => unimplemented("deleteWhere"),
|
|
4419
|
+
deleteDevice: async () => unimplemented("deleteDevice"),
|
|
4420
|
+
allocateId: async () => unimplemented("allocateId")
|
|
4421
|
+
},
|
|
4422
|
+
secrets: makeSpecKeychain(owner).access
|
|
4423
|
+
}),
|
|
4424
|
+
tables,
|
|
4425
|
+
failWrites: (on) => {
|
|
4426
|
+
writesFail = on;
|
|
4427
|
+
},
|
|
4428
|
+
failReads: (on) => {
|
|
4429
|
+
readsFail = on;
|
|
4430
|
+
},
|
|
4431
|
+
seed: (kind, row) => {
|
|
4432
|
+
table(kind).set(row.id, {
|
|
4433
|
+
id: row.id,
|
|
4434
|
+
at: row.at ?? null,
|
|
4435
|
+
rev: 1,
|
|
4436
|
+
updatedAt: 1,
|
|
4437
|
+
data: row.data
|
|
4438
|
+
});
|
|
4439
|
+
}
|
|
4440
|
+
};
|
|
4441
|
+
}
|
|
4442
|
+
//#endregion
|
|
3125
4443
|
//#region src/capabilities/schemas/streaming-shared.ts
|
|
3126
4444
|
/** Shared Zod schemas used across streaming capabilities. */
|
|
3127
4445
|
var CamProfileSchema = z.enum([
|
|
@@ -4281,6 +5599,30 @@ function describeCustomActions(catalog) {
|
|
|
4281
5599
|
return out;
|
|
4282
5600
|
}
|
|
4283
5601
|
//#endregion
|
|
5602
|
+
//#region src/capabilities/caller-stamp.ts
|
|
5603
|
+
/**
|
|
5604
|
+
* Every cap whose provider reads a `callerAddonId` in its input (the keychain's
|
|
5605
|
+
* owner check, device-history's `not-owner`, records' owner).
|
|
5606
|
+
*
|
|
5607
|
+
* The stamp is set ONLY by the transport (`stampCallerAddonId`, or stated by a
|
|
5608
|
+
* hub in-process caller). The generated HTTP/tRPC mount parses input with
|
|
5609
|
+
* `.loose()`, so without this a session could add the key itself and act as
|
|
5610
|
+
* another addon. The mount strips it for these caps.
|
|
5611
|
+
*/
|
|
5612
|
+
var CALLER_STAMPED_CAPABILITIES = [
|
|
5613
|
+
"keychain",
|
|
5614
|
+
"device-history",
|
|
5615
|
+
"records",
|
|
5616
|
+
"integrations",
|
|
5617
|
+
"device-manager"
|
|
5618
|
+
];
|
|
5619
|
+
/** A copy of `input` without the caller stamp. Anything that is not a plain object passes through. */
|
|
5620
|
+
function withoutCallerStamp(input) {
|
|
5621
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) return input;
|
|
5622
|
+
if (!Object.prototype.hasOwnProperty.call(input, "callerAddonId")) return input;
|
|
5623
|
+
return Object.fromEntries(Object.entries(input).filter(([key]) => key !== "callerAddonId"));
|
|
5624
|
+
}
|
|
5625
|
+
//#endregion
|
|
4284
5626
|
//#region src/device/children-batch-max.ts
|
|
4285
5627
|
/**
|
|
4286
5628
|
* Most parents one `getChildrenBatch` may name.
|
|
@@ -4537,94 +5879,6 @@ var RUNTIME_DEFAULTS = {
|
|
|
4537
5879
|
"auth.tokenExpiry": "30d"
|
|
4538
5880
|
};
|
|
4539
5881
|
//#endregion
|
|
4540
|
-
//#region src/device/zod-secret.ts
|
|
4541
|
-
/**
|
|
4542
|
-
* Secret declaration on a Zod config schema (D783).
|
|
4543
|
-
*
|
|
4544
|
-
* A field is a secret because its schema SAYS so — `.meta({ secret: true })`,
|
|
4545
|
-
* spelled {@link SECRET_META} — never because its key looks like a credential.
|
|
4546
|
-
* It is the Zod twin of `secret: true` / `type: 'password'` on a config-UI
|
|
4547
|
-
* field (`isSecretConfigField`), and every consumer that treats secrets
|
|
4548
|
-
* specially reads it: the settings form (`zodEntriesToConfigUI`), the paths a
|
|
4549
|
-
* device advertises at `registerDevice` (`secretConfigPaths`, which the
|
|
4550
|
-
* non-admin redaction withholds at any depth), and the keychain later.
|
|
4551
|
-
*
|
|
4552
|
-
* Zod keeps `.meta()` in `z.globalRegistry`, which lives on `globalThis`, so a
|
|
4553
|
-
* declaration made by an addon's own copy of zod is visible here.
|
|
4554
|
-
*/
|
|
4555
|
-
/**
|
|
4556
|
-
* `{ secret: true }` — the one spelling of "this field holds a credential".
|
|
4557
|
-
*
|
|
4558
|
-
* Addons write the LITERAL `.meta({ secret: true })` rather than importing
|
|
4559
|
-
* this constant: an addon running against an older `@camstack/types` would
|
|
4560
|
-
* otherwise call `.meta(undefined)`, which in zod v4 is the metadata GETTER
|
|
4561
|
-
* and returns no schema at all.
|
|
4562
|
-
*/
|
|
4563
|
-
var SECRET_META = { secret: true };
|
|
4564
|
-
function defOf(schema) {
|
|
4565
|
-
return schema._zod?.def ?? {};
|
|
4566
|
-
}
|
|
4567
|
-
function declaredHere(schema) {
|
|
4568
|
-
return z.globalRegistry.get(schema)?.secret === true;
|
|
4569
|
-
}
|
|
4570
|
-
/** Wrapper kinds whose `innerType` is the field itself. */
|
|
4571
|
-
var WRAPPERS = new Set([
|
|
4572
|
-
"optional",
|
|
4573
|
-
"nullable",
|
|
4574
|
-
"default",
|
|
4575
|
-
"prefault",
|
|
4576
|
-
"catch",
|
|
4577
|
-
"readonly",
|
|
4578
|
-
"nonoptional"
|
|
4579
|
-
]);
|
|
4580
|
-
/**
|
|
4581
|
-
* The schema layers that make up ONE field: the schema itself and every
|
|
4582
|
-
* wrapper below it (optional / default / nullable / readonly / catch), plus
|
|
4583
|
-
* both sides of a pipe (`z.preprocess` puts the real schema on `out`,
|
|
4584
|
-
* `.transform` on `in`).
|
|
4585
|
-
*/
|
|
4586
|
-
function layersOf(schema) {
|
|
4587
|
-
const out = [];
|
|
4588
|
-
const queue = [schema];
|
|
4589
|
-
while (queue.length > 0) {
|
|
4590
|
-
const s = queue.shift();
|
|
4591
|
-
if (!s || out.includes(s)) continue;
|
|
4592
|
-
out.push(s);
|
|
4593
|
-
const def = defOf(s);
|
|
4594
|
-
if (def.type !== void 0 && WRAPPERS.has(def.type) && def.innerType) queue.push(def.innerType);
|
|
4595
|
-
if (def.type === "pipe") {
|
|
4596
|
-
if (def.in) queue.push(def.in);
|
|
4597
|
-
if (def.out) queue.push(def.out);
|
|
4598
|
-
}
|
|
4599
|
-
}
|
|
4600
|
-
return out;
|
|
4601
|
-
}
|
|
4602
|
-
/** True when any layer of this field declares `.meta({ secret: true })`. */
|
|
4603
|
-
function isZodSecret(schema) {
|
|
4604
|
-
return layersOf(schema).some(declaredHere);
|
|
4605
|
-
}
|
|
4606
|
-
/**
|
|
4607
|
-
* Every declared secret path in a config schema: dotted keys, `[]` for "each
|
|
4608
|
-
* element of this array" (`connection.password`, `rows[].token`). A declared
|
|
4609
|
-
* object or array is reported as one path and not descended into — everything
|
|
4610
|
-
* under it is secret.
|
|
4611
|
-
*/
|
|
4612
|
-
function collectZodSecretPaths(schema, prefix = "") {
|
|
4613
|
-
if (isZodSecret(schema)) return prefix.length > 0 ? [prefix] : [];
|
|
4614
|
-
const paths = [];
|
|
4615
|
-
for (const layer of layersOf(schema)) {
|
|
4616
|
-
const def = defOf(layer);
|
|
4617
|
-
if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
|
|
4618
|
-
else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
|
|
4619
|
-
else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
|
|
4620
|
-
else if (def.type === "intersection") {
|
|
4621
|
-
if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
|
|
4622
|
-
if (def.right) paths.push(...collectZodSecretPaths(def.right, prefix));
|
|
4623
|
-
}
|
|
4624
|
-
}
|
|
4625
|
-
return [...new Set(paths)];
|
|
4626
|
-
}
|
|
4627
|
-
//#endregion
|
|
4628
5882
|
//#region src/device/device-state-handle.ts
|
|
4629
5883
|
var DEVICE_STATE_EVENT_CATEGORY = "device.state-changed";
|
|
4630
5884
|
/**
|
|
@@ -4871,6 +6125,50 @@ function isSource(x) {
|
|
|
4871
6125
|
return typeof x.read === "function" && typeof x.watch === "function";
|
|
4872
6126
|
}
|
|
4873
6127
|
//#endregion
|
|
6128
|
+
//#region src/helpers/addon-slice-handle.ts
|
|
6129
|
+
/** Structural bridge: every `AddonApi` client carries `deviceState` (+ optional `live`). */
|
|
6130
|
+
function addonSliceHandleApi(api) {
|
|
6131
|
+
return api;
|
|
6132
|
+
}
|
|
6133
|
+
//#endregion
|
|
6134
|
+
//#region src/helpers/trpc-leaf.ts
|
|
6135
|
+
function isTrpcProcedureLeaf(value) {
|
|
6136
|
+
if (typeof value !== "object" || value === null) return false;
|
|
6137
|
+
const record = value;
|
|
6138
|
+
return (typeof record.query === "function" || typeof record.mutate === "function" || typeof record.subscribe === "function") && !Array.isArray(value);
|
|
6139
|
+
}
|
|
6140
|
+
function readCapRecord(api, capProp) {
|
|
6141
|
+
const cap = Reflect.get(api, capProp);
|
|
6142
|
+
if (typeof cap !== "object" || cap === null || Array.isArray(cap)) return void 0;
|
|
6143
|
+
return cap;
|
|
6144
|
+
}
|
|
6145
|
+
function readTrpcLeaf(api, capProp, method) {
|
|
6146
|
+
const capRecord = readCapRecord(api, capProp);
|
|
6147
|
+
if (!capRecord) throw new Error(`tRPC client has no router '${capProp}'`);
|
|
6148
|
+
const leaf = capRecord[method];
|
|
6149
|
+
if (!isTrpcProcedureLeaf(leaf)) throw new Error(`tRPC client has no '${capProp}.${method}'`);
|
|
6150
|
+
return leaf;
|
|
6151
|
+
}
|
|
6152
|
+
/**
|
|
6153
|
+
* Invoke `api.<capProp>.<method>.{query|mutate|subscribe}(input)`.
|
|
6154
|
+
* `Result` is the generated proxy method's return. The leaf itself is only
|
|
6155
|
+
* `unknown` (the client is a structural bag), so the assertion is the one
|
|
6156
|
+
* boundary between that bag and the typed proxy.
|
|
6157
|
+
*/
|
|
6158
|
+
function invokeTrpcLeaf(api, capProp, method, kind, input, push) {
|
|
6159
|
+
const fn = readTrpcLeaf(api, capProp, method);
|
|
6160
|
+
if (kind === "mutation") {
|
|
6161
|
+
if (typeof fn.mutate !== "function") throw new Error(`tRPC '${capProp}.${method}' has no mutate()`);
|
|
6162
|
+
return fn.mutate(input);
|
|
6163
|
+
}
|
|
6164
|
+
if (kind === "subscription") {
|
|
6165
|
+
if (typeof fn.subscribe !== "function") throw new Error(`tRPC '${capProp}.${method}' has no subscribe()`);
|
|
6166
|
+
return fn.subscribe(input, push);
|
|
6167
|
+
}
|
|
6168
|
+
if (typeof fn.query !== "function") throw new Error(`tRPC '${capProp}.${method}' has no query()`);
|
|
6169
|
+
return fn.query(input);
|
|
6170
|
+
}
|
|
6171
|
+
//#endregion
|
|
4874
6172
|
//#region src/generated/device-proxy.ts
|
|
4875
6173
|
/**
|
|
4876
6174
|
* Build a DeviceProxy that pre-binds deviceId + nodeId on every method call
|
|
@@ -4888,8 +6186,7 @@ function isSource(x) {
|
|
|
4888
6186
|
* field on the returned object.
|
|
4889
6187
|
*/
|
|
4890
6188
|
function createDeviceProxy(api, binding, opts) {
|
|
4891
|
-
const
|
|
4892
|
-
const stateSource = opts?.stateSource ?? createLazyTrpcSource(api);
|
|
6189
|
+
const stateSource = opts?.stateSource ?? createLazyTrpcSource(addonSliceHandleApi(api));
|
|
4893
6190
|
/** Merge `{deviceId, nodeId?}` into the caller-supplied input. */
|
|
4894
6191
|
function mergeInput(input, nodeId) {
|
|
4895
6192
|
const base = typeof input === "object" && input !== null ? input : {};
|
|
@@ -4902,20 +6199,8 @@ function createDeviceProxy(api, binding, opts) {
|
|
|
4902
6199
|
deviceId: binding.deviceId
|
|
4903
6200
|
};
|
|
4904
6201
|
}
|
|
4905
|
-
/**
|
|
4906
|
-
* Invoke `api.<capProp>.<method>.{query|mutate|subscribe}(merged)`.
|
|
4907
|
-
* Returns `any` so the caller-side per-method declaration assigns
|
|
4908
|
-
* cleanly into the strongly-typed `InferDeviceProxyCap<...>` shape on
|
|
4909
|
-
* the `DeviceProxy` interface — the proxy's contract is enforced by
|
|
4910
|
-
* the interface, not the dispatch helper.
|
|
4911
|
-
*/
|
|
4912
6202
|
function callLeaf(capProp, method, kind, merged, push) {
|
|
4913
|
-
|
|
4914
|
-
if (!leaf) throw new Error(`DeviceProxy: api has no '${capProp}.${method}'`);
|
|
4915
|
-
const fn = leaf;
|
|
4916
|
-
if (kind === "mutation") return fn.mutate(merged);
|
|
4917
|
-
if (kind === "subscription") return fn.subscribe(merged, push);
|
|
4918
|
-
return fn.query(merged);
|
|
6203
|
+
return invokeTrpcLeaf(api, capProp, method, kind, merged, push);
|
|
4919
6204
|
}
|
|
4920
6205
|
/**
|
|
4921
6206
|
* Device-scoped cap dispatch. Looks up the binding entry for `capName`
|
|
@@ -6187,4 +7472,4 @@ function sleepCancellable(ms, signal) {
|
|
|
6187
7472
|
});
|
|
6188
7473
|
}
|
|
6189
7474
|
//#endregion
|
|
6190
|
-
export {
|
|
7475
|
+
export { optionalMethod as $, PEER_BYTES_CHUNK_BYTES as $n, schemaDeclaresAnyField as $r, REMOVE_LEAF as $t, HF_REPO as A, RECORD_BATCH_MAX_BYTES as An, emitReadiness as Ar, makeProfileBrokerId as At, describeCustomActions as B, RECORD_SECRET_COLLECTION_PREFIX as Bn, SECRET_CLEAR as Br, settingsBrokersAdoptsFrom as Bt, addonSliceHandleApi as C, RecordsError as Cn, globalSecretScope as Cr, ProfileRtspEntrySchema as Ct, createSliceHandle as D, asRecordsError as Dn, normalizeNodeId as Dr, StreamSourceSchema as Dt, createMirrorSource as E, UnknownRecordKindError as En, parseSecretRef as Er, StreamSourceEntrySchema$1 as Et, DEVICE_CHILDREN_BATCH_MAX as F, RECORD_ID_MAX_LENGTH as Fn, collectHydratedFieldValues as Fr, SETTINGS_ARRAY_ADOPTS_PREFIX as Ft, DEVICE_STATUS_METHOD as G, createSecretsAccess as Gn, isSecretMarkerLike as Gr, RecordFieldPathSchema as Gt, adminUiCapability as H, RECORD_SWEEP_INTERVAL_MS as Hn, isSecretClear as Hr, entryBody as Ht, CALLER_STAMPED_CAPABILITIES as I, RECORD_MAX_DATA_BYTES as In, dehydrateConfigField as Ir, parseSettingsArrayAdoptsFrom as It, PROVIDER_PIN_FIELD as J, forgetKnownSecret as Jn, REDACTED_SECRET as Jr, RecordKindNameSchema as Jt, HISTORY_DEFAULT_MIN_INTERVAL_MS as K, MIN_SCRUBBED_LENGTH as Kn, makeSecretMarker as Kr, RecordIndexSchema as Kt, withoutCallerStamp as L, RECORD_MAX_INDEXES as Ln, dehydrateSchema as Lr, settingsArrayAdoptsFrom as Lt, RawStateResultSchema as M, RECORD_DATABASES as Mn, WELL_KNOWN_TABS as Mr, parseProfileBrokerId as Mt, deviceOpsCapability as N, RECORD_DEFAULT_MAX_DATA_BYTES as Nn, WELL_KNOWN_TAB_MAP as Nr, selectAssignedProfileSlots as Nt, DEFAULT_TOKEN_EXPIRY as O, recordsErrorFromMessage as On, createDurableState as Or, SubscribeAudioChunksInputSchema as Ot, viewerUiCapability as P, RECORD_DELETE_BATCH as Pn, collectHydratedFieldEntries as Pr, makeInMemoryRecords as Pt, method as Q, shortSecretRefHash as Qn, isSecretConfigField as Qr, defineRecordKinds as Qt, customAction as R, RECORD_MAX_INDEX_FIELDS as Rn, hydrateSchema as Rr, SETTINGS_BROKERS_ADOPTS_PREFIX as Rt, invokeTrpcLeaf as S, RecordUniqueViolationError as Sn, formatSecretScope as Sr, FrameHandleSchema as St, createLazyTrpcSource as T, UnindexedFilterError as Tn, normalizeSecretOwner as Tr, ProfileSlotStatusSchema as Tt, DEFAULT_RUNTIME_STATE_DURABILITY as U, resolveSecretViewsIn as Un, isSecretFieldView as Ur, createRecordsAccess as Ut, deviceCustomAction as V, RECORD_SETTINGS_MAX_ROWS as Vn, classifySecretWrite as Vr, defineEntryKind as Vt, DEVICE_SETTINGS_CONTRIBUTION_METHODS as W, REVALIDATE_AFTER_MS as Wn, isSecretMarker as Wr, EntryDescriptorSchema as Wt, expandCapMethods as X, registerKnownSecret as Xn, collectSecretConfigKeys as Xr, assertValidDeclaration as Xt, event as Y, knownSecretCount as Yn, collectKeychainStoredKeys as Yr, RecordRetentionSchema as Yt, isDeviceConfigCap as Z, scrubKnownSecrets as Zn, isKeychainStoredField as Zr, declarationsOf as Zt, readErrorI18n as _, PlaintextSecretError as _n, resolveSecretMarkers as _r, CamProfileSchema as _t, asJsonArray as a, recordSecretRef as an, SHARE_VIEW_KINDS as ar, toNodeId as at, isDeviceScopedCap as b, RecordSecretUnavailableError as bn, arrayItemFieldPath as br, CameraStreamSchema as bt, asString as c, collectZodSecretPaths as cn, classifyStoreError as cr, ReadinessTimeoutError as ct, parseJsonUnknown as d, BatchTooLargeError as dn, parseSecretStatePath as dr, scopeKey as dt, AUDIO_CHUNK_FORMATS as ei, collectSecretLeaves as en, PEER_BYTES_MAX_OUTSTANDING as er, resolveCapMount as et, hfModelUrl as f, DeclarationRefusedError as fn, secretTreeFromPaths as fr, AudioChunkFormatSchema as ft, ErrorI18nPayloadSchema as g, NoCallerError as gn, isSecretRefOwnedBy as gr, CAM_PROFILE_ORDER as gt, ErrorCodeSchema as h, InvalidRecordError as hn, collectSecretMarkerRefs as hr, BrokerStatusSchema as ht, asBoolean as i, recordSecretCollection as in, DATAPLANE_SECRET_HEADER as ir, readNodePin as it, RUNTIME_DEFAULTS as j, RECORD_CLASSES as jn, isEvent as jr, makeSourceBrokerId as jt, HF_BASE_URL as k, RECORD_BATCH_MAX as kn, createEvent as kr, SubscribeAudioChunksResultSchema as kt, parseJsonArray as l, isZodSecret as ln, normalizeAddonInitResult as lr, emitDownForOwnedCaps as lt, CamstackError as m, ForeignSecretMarkerError as mn, KeychainWriteRefusedError as mr, BrokerStatsSchema as mt, sleepCancellable as n, expandAudioChunkToF32le as ni, parseSecretPath as nn, PEER_BYTES_TICKET_TTL_MS as nr, CAP_NODE_PIN_CONTEXT_KEY as nt, asJsonObject as o, secretArrayPrefixes as on, DisposerChain as or, MODEL_FORMATS as ot, BOOT_RECOVERY_BACKOFF_MS as p, FilterRequiredError as pn, secretTreeFromSchema as pr, AudioStreamPickSchema as pt, HISTORY_UNIT_LABELS as q, clearKnownSecrets as qn, toSecretFieldView as qr, RecordKindDeclarationSchema as qt, maskUrlCredentials as r, isAudioChunkFormat as ri, recordArrayItemPath as rn, PeerBytesTicketSchema as rr, nodePin as rt, asNumber as s, SECRET_META as sn, BaseAddon as sr, ReadinessRegistry as st, sleep as t, audioChunkBytesPerSample as ti, mapSecretLeaves as tn, PEER_BYTES_STALL_MS as tr, systemMethod as tt, parseJsonObject as u, AdoptionPendingError as un, mergeSecretStatePaths as ur, readinessKey as ut, userError as v, RECORDS_ERROR_CODES as vn, transformSecretWrite as vr, CamStreamKindSchema as vt, createEventBusSliceSource as w, RevConflictError as wn, isSecretRef as wr, ProfileSlotSchema as wt, createDeviceProxy as x, RecordTooLargeError as xn, buildSecretRef as xr, DecodedAudioChunkSchema as xt, DEVICE_SCOPED_CAPS as y, RecordNotFoundError as yn, SECRET_REF_SCHEME as yr, CamStreamResolutionSchema as yt, defineCustomActions as z, RECORD_QUERY_MAX as zn, resolveHydratedFieldValue as zr, parseSettingsBrokersAdoptsFrom as zt };
|