@camstack/types 1.2.379 → 1.2.380
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 +1 -0
- package/dist/addon.js +57 -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 +246 -0
- package/dist/client.js +6 -0
- package/dist/client.mjs +6 -0
- package/dist/generated/addon-api.d.ts +177 -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/index.d.ts +1 -0
- package/dist/index.js +685 -2
- package/dist/index.mjs +617 -3
- package/dist/interfaces/addon.d.ts +13 -0
- package/dist/interfaces/event-bus.d.ts +32 -3
- package/dist/interfaces/storage.d.ts +1 -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/index.d.ts +6 -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/{sleep-DQYi915G.js → sleep-CIGOw58l.js} +1503 -298
- package/dist/{sleep-D-Q2_Mcd.mjs → sleep-WsrozGDL.mjs} +999 -112
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ import type { PipelineSlot } from '../types/pipeline.js';
|
|
|
5
5
|
import type { AddonDataPlane } from './addon-data-plane.js';
|
|
6
6
|
import type { AddonPeerBytes } from './addon-peer-bytes.js';
|
|
7
7
|
import type { SecretsAccess } from '../keychain/secrets-access.js';
|
|
8
|
+
import type { RecordsAccess } from '../records/records-access.js';
|
|
8
9
|
import type { CapabilityDeclaration } from './capability.js';
|
|
9
10
|
import type { ConfigUISchemaWithValues, FieldProbeResult } from './config-ui.js';
|
|
10
11
|
import type { IEventBus, ReadinessScope } from './event-bus.js';
|
|
@@ -733,6 +734,18 @@ export interface AddonContext<TConfig = Record<string, unknown>> {
|
|
|
733
734
|
* kernel that predates the keychain.
|
|
734
735
|
*/
|
|
735
736
|
readonly secrets?: SecretsAccess;
|
|
737
|
+
/**
|
|
738
|
+
* This addon's record kinds (D881): its own facts and documents - ledgers,
|
|
739
|
+
* audit rows, config documents, mirrors - read by key, by device, by device
|
|
740
|
+
* and time. The owner is the addon's own id, closed over by the kernel; a kind
|
|
741
|
+
* is readable and writable by its owner only. A field marked
|
|
742
|
+
* `.meta({ secret: true })` in a kind's schema is kept in the keychain
|
|
743
|
+
* automatically. See `docs/architecture/records.md`.
|
|
744
|
+
*
|
|
745
|
+
* Absent when the addon is loaded in isolation (tests, tooling) or on a
|
|
746
|
+
* kernel that predates records.
|
|
747
|
+
*/
|
|
748
|
+
readonly records?: RecordsAccess;
|
|
736
749
|
/**
|
|
737
750
|
* Runtime accessor for the capability registry — exposed to addons that
|
|
738
751
|
* need to enumerate peers of a collection cap (e.g. `turn-provider`) or
|
|
@@ -751,8 +751,21 @@ export interface TopologyNode {
|
|
|
751
751
|
* - `resumed` — demand returned and the dial came back. The matching
|
|
752
752
|
* end of a `suspended`, and equally deliberate.
|
|
753
753
|
* - `broker-destroyed` — the stream was unpublished/released. Deliberate.
|
|
754
|
+
*
|
|
755
|
+
* A BATTERY camera sleeping is none of these: a sleep or a plain wake emits no
|
|
756
|
+
* `stream.offline` / `stream.online` at all (D883). It reaches an outage only
|
|
757
|
+
* through the three battery reasons below, each naming a real failure:
|
|
758
|
+
*
|
|
759
|
+
* - `wake-timeout` — a wake was DEMANDED (live viewer, recording,
|
|
760
|
+
* explicit force) and no stream arrived inside its bound.
|
|
761
|
+
* - `connection-error` — the camera is reported awake and its stream is
|
|
762
|
+
* gone.
|
|
763
|
+
* - `battery-unreachable` — nothing heard from the camera past its battery
|
|
764
|
+
* refresh window (`BATTERY_UNREACHABLE_AFTER_MS`, D847).
|
|
765
|
+
*
|
|
766
|
+
* Their `stream.online` is `recovered`, exactly as for `stale-timeout`.
|
|
754
767
|
*/
|
|
755
|
-
export type StreamHealthReason = 'stale-timeout' | 'recovered' | 'first-packet' | 'suspended' | 'resumed' | 'broker-destroyed';
|
|
768
|
+
export type StreamHealthReason = 'stale-timeout' | 'wake-timeout' | 'connection-error' | 'battery-unreachable' | 'recovered' | 'first-packet' | 'suspended' | 'resumed' | 'broker-destroyed';
|
|
756
769
|
/** All known event categories with their typed payloads */
|
|
757
770
|
/**
|
|
758
771
|
* `keychain.changed` — a value was put or deleted. Never carries the value.
|
|
@@ -1556,21 +1569,37 @@ export interface EventCatalog {
|
|
|
1556
1569
|
*/
|
|
1557
1570
|
'stream.online': {
|
|
1558
1571
|
readonly deviceId: number;
|
|
1559
|
-
|
|
1572
|
+
/** Absent when a failed wake names no published stream (a sleeping camera after a restart). */
|
|
1573
|
+
readonly camStreamId?: string;
|
|
1560
1574
|
readonly profile: CamProfile | null;
|
|
1561
1575
|
readonly brokerId: string;
|
|
1562
1576
|
readonly sourceType: string;
|
|
1563
1577
|
readonly lastPacketAt: number;
|
|
1564
1578
|
readonly reason?: StreamHealthReason;
|
|
1579
|
+
/**
|
|
1580
|
+
* Every stream of the device this one transition covers. An outage and its
|
|
1581
|
+
* recovery are DEVICE transitions (D883): main and sub dropping together is
|
|
1582
|
+
* ONE event whose `camStreamId` / `profile` name the first and this list
|
|
1583
|
+
* names them all. Absent on per-broker lazy-dial telemetry.
|
|
1584
|
+
*/
|
|
1585
|
+
readonly camStreamIds?: readonly string[];
|
|
1565
1586
|
};
|
|
1566
1587
|
'stream.offline': {
|
|
1567
1588
|
readonly deviceId: number;
|
|
1568
|
-
|
|
1589
|
+
/** Absent when a failed wake names no published stream (a sleeping camera after a restart). */
|
|
1590
|
+
readonly camStreamId?: string;
|
|
1569
1591
|
readonly profile: CamProfile | null;
|
|
1570
1592
|
readonly brokerId: string;
|
|
1571
1593
|
readonly sourceType: string;
|
|
1572
1594
|
readonly lastPacketAt: number;
|
|
1573
1595
|
readonly reason?: StreamHealthReason;
|
|
1596
|
+
/**
|
|
1597
|
+
* Every stream of the device this one transition covers. An outage and its
|
|
1598
|
+
* recovery are DEVICE transitions (D883): main and sub dropping together is
|
|
1599
|
+
* ONE event whose `camStreamId` / `profile` name the first and this list
|
|
1600
|
+
* names them all. Absent on per-broker lazy-dial telemetry.
|
|
1601
|
+
*/
|
|
1602
|
+
readonly camStreamIds?: readonly string[];
|
|
1574
1603
|
};
|
|
1575
1604
|
'retention.cleanup': {
|
|
1576
1605
|
readonly deletedEvents?: number;
|
|
@@ -49,7 +49,7 @@ export interface IStorageProvider {
|
|
|
49
49
|
* All named collections that the settings backend manages.
|
|
50
50
|
* Each collection maps to a logical table/bucket in the backend.
|
|
51
51
|
*/
|
|
52
|
-
export type SettingsCollection = 'system-settings' | 'addon-settings' | '
|
|
52
|
+
export type SettingsCollection = 'system-settings' | 'addon-settings' | 'detection-events' | 'audio-levels' | 'track-trails' | 'recording-segments' | 'recording-thumbnails' | 'recording-policies' | 'recording-storage-config' | 'recording-cleanup-queue' | 'known-faces' | (string & {});
|
|
53
53
|
export interface SettingsGetInput {
|
|
54
54
|
readonly namespace?: string;
|
|
55
55
|
readonly collection: SettingsCollection;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
|
|
2
|
+
/** The files a kind may live in. `history` is device-history's (D850), never records. */
|
|
3
|
+
export declare const RECORD_DATABASES: readonly ['settings', 'events'];
|
|
4
|
+
export type RecordDatabase = (typeof RECORD_DATABASES)[number];
|
|
5
|
+
/** The classes of a kind: the `@durable` vocabulary, one authority (replaces the annotation). */
|
|
6
|
+
export declare const RECORD_CLASSES: readonly ['config', 'ledger', 'audit', 'mirror', 'ephemeral'];
|
|
7
|
+
export type RecordClass = (typeof RECORD_CLASSES)[number];
|
|
8
|
+
export declare const RECORD_DEFAULT_MAX_DATA_BYTES: number;
|
|
9
|
+
export declare const RECORD_MAX_DATA_BYTES: number;
|
|
10
|
+
export declare const RECORD_ID_MAX_LENGTH = 256;
|
|
11
|
+
export declare const RECORD_BATCH_MAX = 1000;
|
|
12
|
+
export declare const RECORD_BATCH_MAX_BYTES: number;
|
|
13
|
+
export declare const RECORD_QUERY_MAX = 1000;
|
|
14
|
+
/** Rows one DELETE statement removes before the serving thread yields. */
|
|
15
|
+
export declare const RECORD_DELETE_BATCH = 5000;
|
|
16
|
+
/** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
|
|
17
|
+
export declare const RECORD_SETTINGS_MAX_ROWS = 50000;
|
|
18
|
+
export declare const RECORD_MAX_INDEXES = 8;
|
|
19
|
+
export declare const RECORD_MAX_INDEX_FIELDS = 4;
|
|
20
|
+
/** The engine's TTL sweep cadence. */
|
|
21
|
+
export declare const RECORD_SWEEP_INTERVAL_MS: number;
|
|
22
|
+
/** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
|
|
23
|
+
export declare const RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry kinds (D881): every list of user-created entries (notification
|
|
3
|
+
* targets, brokers, LLM profiles) is ONE record kind with an id, a name and a
|
|
4
|
+
* kind-specific body, and the admin UI lists/adds/edits/deletes all of them
|
|
5
|
+
* through the `record-entries` capability with password fields derived from
|
|
6
|
+
* the schema (`.meta({ secret: true })`).
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import type { RecordIndex, DefinedRecordKind } from './record-kind.js';
|
|
10
|
+
/** What an entry kind adds to a bare record kind. */
|
|
11
|
+
export interface EntryKindSpec<S extends z.ZodType> {
|
|
12
|
+
/** The title the entries UI shows for the list. */
|
|
13
|
+
readonly title: string;
|
|
14
|
+
/** The body: a `name` field plus the kind's own. A discriminated union is fine. */
|
|
15
|
+
readonly schema: S;
|
|
16
|
+
readonly writeTrigger?: string;
|
|
17
|
+
readonly nameField?: string;
|
|
18
|
+
readonly database?: 'settings' | 'events';
|
|
19
|
+
readonly indexes?: readonly RecordIndex[];
|
|
20
|
+
readonly secretArrayKeys?: Readonly<Record<string, string>>;
|
|
21
|
+
readonly adoptsFrom?: string;
|
|
22
|
+
}
|
|
23
|
+
/** The body of a plain entry: a name and the fields the kind adds. */
|
|
24
|
+
export declare function entryBody<Shape extends z.ZodRawShape>(shape: Shape): z.ZodObject<{
|
|
25
|
+
name: z.ZodString;
|
|
26
|
+
} & Shape extends infer T ? { -readonly [P in keyof T]: T[P]; } : never, z.core.$strip>;
|
|
27
|
+
/**
|
|
28
|
+
* Declare one entry kind. An entry is configuration: never trimmed by the
|
|
29
|
+
* engine, never aged, belonging to no device.
|
|
30
|
+
*/
|
|
31
|
+
export declare function defineEntryKind<S extends z.ZodType>(name: string, spec: EntryKindSpec<S>): DefinedRecordKind<S>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The named failures of `ctx.records` (D881).
|
|
3
|
+
*
|
|
4
|
+
* A records call crosses a worker thread, a UDS channel and a tRPC router, and
|
|
5
|
+
* an error class does not survive any of them - only its message does. So
|
|
6
|
+
* every message STARTS with a stable code, `records/<code>: <detail>`, and
|
|
7
|
+
* {@link recordsErrorFromMessage} rebuilds the typed class on the caller's side.
|
|
8
|
+
* A failed read is never `[]` or `undefined`: it throws one of these.
|
|
9
|
+
*/
|
|
10
|
+
/** Every code a records failure carries. */
|
|
11
|
+
export declare const RECORDS_ERROR_CODES: readonly ['rev-conflict', 'too-large', 'unindexed-filter', 'filter-required', 'unknown-kind', 'not-found', 'unique-violation', 'plaintext-secret', 'foreign-marker', 'secret-unavailable', 'invalid-record', 'declaration-refused', 'adoption-pending', 'no-caller', 'batch-too-large'];
|
|
12
|
+
export type RecordsErrorCode = (typeof RECORDS_ERROR_CODES)[number];
|
|
13
|
+
/** The base of every records failure. `detail` is the human part of the message. */
|
|
14
|
+
export declare class RecordsError extends Error {
|
|
15
|
+
readonly code: RecordsErrorCode;
|
|
16
|
+
readonly detail: string;
|
|
17
|
+
constructor(code: RecordsErrorCode, detail: string);
|
|
18
|
+
}
|
|
19
|
+
/** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
|
|
20
|
+
export declare class RevConflictError extends RecordsError {
|
|
21
|
+
constructor(detail: string);
|
|
22
|
+
}
|
|
23
|
+
/** `data` exceeds the kind's `maxDataBytes`. Bytes are never record fields (D613). */
|
|
24
|
+
export declare class RecordTooLargeError extends RecordsError {
|
|
25
|
+
constructor(detail: string);
|
|
26
|
+
}
|
|
27
|
+
/** A filter, order or aggregate names a field no declared index serves. */
|
|
28
|
+
export declare class UnindexedFilterError extends RecordsError {
|
|
29
|
+
constructor(detail: string);
|
|
30
|
+
}
|
|
31
|
+
/** A bulk delete with no predicate at all. */
|
|
32
|
+
export declare class FilterRequiredError extends RecordsError {
|
|
33
|
+
constructor(detail: string);
|
|
34
|
+
}
|
|
35
|
+
/** The caller declared no such kind (another addon's kind is indistinguishable from none). */
|
|
36
|
+
export declare class UnknownRecordKindError extends RecordsError {
|
|
37
|
+
constructor(detail: string);
|
|
38
|
+
}
|
|
39
|
+
export declare class RecordNotFoundError extends RecordsError {
|
|
40
|
+
constructor(detail: string);
|
|
41
|
+
}
|
|
42
|
+
/** A unique index refused the write. */
|
|
43
|
+
export declare class RecordUniqueViolationError extends RecordsError {
|
|
44
|
+
constructor(detail: string);
|
|
45
|
+
}
|
|
46
|
+
/** A secret field held a plain string where only a keychain marker may be stored. */
|
|
47
|
+
export declare class PlaintextSecretError extends RecordsError {
|
|
48
|
+
constructor(detail: string);
|
|
49
|
+
}
|
|
50
|
+
/** A marker whose ref is not the row's own (the D878 rule). */
|
|
51
|
+
export declare class ForeignSecretMarkerError extends RecordsError {
|
|
52
|
+
constructor(detail: string);
|
|
53
|
+
}
|
|
54
|
+
/** The keychain could not store or serve a value: explicit, never an empty string. */
|
|
55
|
+
export declare class RecordSecretUnavailableError extends RecordsError {
|
|
56
|
+
constructor(detail: string);
|
|
57
|
+
}
|
|
58
|
+
export declare class InvalidRecordError extends RecordsError {
|
|
59
|
+
constructor(detail: string);
|
|
60
|
+
}
|
|
61
|
+
/** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
|
|
62
|
+
export declare class DeclarationRefusedError extends RecordsError {
|
|
63
|
+
constructor(detail: string);
|
|
64
|
+
}
|
|
65
|
+
/** A kind is empty while the legacy table it adopts from still holds rows. */
|
|
66
|
+
export declare class AdoptionPendingError extends RecordsError {
|
|
67
|
+
constructor(detail: string);
|
|
68
|
+
}
|
|
69
|
+
export declare class NoCallerError extends RecordsError {
|
|
70
|
+
constructor(detail: string);
|
|
71
|
+
}
|
|
72
|
+
export declare class BatchTooLargeError extends RecordsError {
|
|
73
|
+
constructor(detail: string);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Rebuild the typed error from a message that crossed a wire; `null` when the
|
|
77
|
+
* message is not a records failure.
|
|
78
|
+
*/
|
|
79
|
+
export declare function recordsErrorFromMessage(message: string): RecordsError | null;
|
|
80
|
+
/** The records failure inside any thrown value (a wrapped tRPC error included), or the value. */
|
|
81
|
+
export declare function asRecordsError(err: unknown): unknown;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Record kinds (D881): the declaration an addon makes of one kind of fact, the
|
|
3
|
+
* Zod row schema that describes it, and the helper that turns the two into the
|
|
4
|
+
* JSON the engine stores.
|
|
5
|
+
*
|
|
6
|
+
* A kind is declared ONCE, next to the code that owns it, and the engine's
|
|
7
|
+
* reconciler creates its indexes - nothing creates DDL at runtime from a write.
|
|
8
|
+
* Everything the generic machinery needs (secret fields, the entry descriptor)
|
|
9
|
+
* is DERIVED from the Zod schema: `.meta({ secret: true })` marks a credential,
|
|
10
|
+
* never a field name (D879).
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { RECORD_CLASSES, RECORD_DATABASES } from './constants.js';
|
|
14
|
+
/** A JSON field path: `trackId`, `auth.accessToken`. Charset-restricted: it is rendered into DDL. */
|
|
15
|
+
export declare const RecordFieldPathSchema: z.ZodString;
|
|
16
|
+
export declare const RecordIndexSchema: z.ZodObject<{
|
|
17
|
+
name: z.ZodString;
|
|
18
|
+
fields: z.ZodArray<z.ZodString>;
|
|
19
|
+
unique: z.ZodOptional<z.ZodBoolean>;
|
|
20
|
+
}, z.core.$strict>;
|
|
21
|
+
export type RecordIndex = z.infer<typeof RecordIndexSchema>;
|
|
22
|
+
export declare const RecordRetentionSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
23
|
+
ttlMs: z.ZodNumber;
|
|
24
|
+
from: z.ZodEnum<{
|
|
25
|
+
at: "at";
|
|
26
|
+
write: "write";
|
|
27
|
+
}>;
|
|
28
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
29
|
+
maxRows: z.ZodOptional<z.ZodNumber>;
|
|
30
|
+
maxRowsPerDevice: z.ZodOptional<z.ZodNumber>;
|
|
31
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
32
|
+
owner: z.ZodString;
|
|
33
|
+
reason: z.ZodString;
|
|
34
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
35
|
+
none: z.ZodString;
|
|
36
|
+
}, z.core.$strict>]>;
|
|
37
|
+
export type RecordRetention = z.infer<typeof RecordRetentionSchema>;
|
|
38
|
+
/** The descriptor a generic entries UI reads (D881 "generic entries"). */
|
|
39
|
+
export declare const EntryDescriptorSchema: z.ZodObject<{
|
|
40
|
+
title: z.ZodString;
|
|
41
|
+
nameField: z.ZodString;
|
|
42
|
+
fields: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
43
|
+
}, z.core.$strict>;
|
|
44
|
+
export type EntryDescriptor = z.infer<typeof EntryDescriptorSchema>;
|
|
45
|
+
/** What crosses the wire and sits in the engine's catalog: JSON only, no Zod. */
|
|
46
|
+
export declare const RecordKindDeclarationSchema: z.ZodObject<{
|
|
47
|
+
database: z.ZodDefault<z.ZodEnum<{
|
|
48
|
+
events: "events";
|
|
49
|
+
settings: "settings";
|
|
50
|
+
}>>;
|
|
51
|
+
class: z.ZodEnum<{
|
|
52
|
+
audit: "audit";
|
|
53
|
+
config: "config";
|
|
54
|
+
ephemeral: "ephemeral";
|
|
55
|
+
ledger: "ledger";
|
|
56
|
+
mirror: "mirror";
|
|
57
|
+
}>;
|
|
58
|
+
writeTrigger: z.ZodString;
|
|
59
|
+
device: z.ZodDefault<z.ZodEnum<{
|
|
60
|
+
none: "none";
|
|
61
|
+
optional: "optional";
|
|
62
|
+
required: "required";
|
|
63
|
+
}>>;
|
|
64
|
+
at: z.ZodDefault<z.ZodEnum<{
|
|
65
|
+
none: "none";
|
|
66
|
+
required: "required";
|
|
67
|
+
}>>;
|
|
68
|
+
indexes: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
69
|
+
name: z.ZodString;
|
|
70
|
+
fields: z.ZodArray<z.ZodString>;
|
|
71
|
+
unique: z.ZodOptional<z.ZodBoolean>;
|
|
72
|
+
}, z.core.$strict>>>;
|
|
73
|
+
retention: z.ZodUnion<readonly [z.ZodObject<{
|
|
74
|
+
ttlMs: z.ZodNumber;
|
|
75
|
+
from: z.ZodEnum<{
|
|
76
|
+
at: "at";
|
|
77
|
+
write: "write";
|
|
78
|
+
}>;
|
|
79
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
80
|
+
maxRows: z.ZodOptional<z.ZodNumber>;
|
|
81
|
+
maxRowsPerDevice: z.ZodOptional<z.ZodNumber>;
|
|
82
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
83
|
+
owner: z.ZodString;
|
|
84
|
+
reason: z.ZodString;
|
|
85
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
86
|
+
none: z.ZodString;
|
|
87
|
+
}, z.core.$strict>]>;
|
|
88
|
+
deviceRemoval: z.ZodDefault<z.ZodEnum<{
|
|
89
|
+
delete: "delete";
|
|
90
|
+
none: "none";
|
|
91
|
+
}>>;
|
|
92
|
+
maxDataBytes: z.ZodDefault<z.ZodNumber>;
|
|
93
|
+
secretPaths: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
94
|
+
secretArrayKeys: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
95
|
+
adoptsFrom: z.ZodOptional<z.ZodString>;
|
|
96
|
+
transactFamily: z.ZodOptional<z.ZodString>;
|
|
97
|
+
entry: z.ZodOptional<z.ZodObject<{
|
|
98
|
+
title: z.ZodString;
|
|
99
|
+
nameField: z.ZodString;
|
|
100
|
+
fields: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
101
|
+
}, z.core.$strict>>;
|
|
102
|
+
}, z.core.$strict>;
|
|
103
|
+
export type RecordKindDeclaration = z.infer<typeof RecordKindDeclarationSchema>;
|
|
104
|
+
/** The kind names: lower-case, dash separated, short enough for an index name. */
|
|
105
|
+
export declare const RecordKindNameSchema: z.ZodString;
|
|
106
|
+
/** Cross-field rules a declaration must satisfy. Throws {@link DeclarationRefusedError}. */
|
|
107
|
+
export declare function assertValidDeclaration(name: string, decl: RecordKindDeclaration): void;
|
|
108
|
+
/** The input of {@link defineRecordKinds} for one kind. */
|
|
109
|
+
export interface RecordKindSpec<S extends z.ZodType = z.ZodType> {
|
|
110
|
+
readonly schema: S;
|
|
111
|
+
readonly database?: (typeof RECORD_DATABASES)[number];
|
|
112
|
+
readonly class: (typeof RECORD_CLASSES)[number];
|
|
113
|
+
readonly writeTrigger: string;
|
|
114
|
+
readonly device?: 'required' | 'optional' | 'none';
|
|
115
|
+
readonly at?: 'required' | 'none';
|
|
116
|
+
readonly indexes?: readonly RecordIndex[];
|
|
117
|
+
readonly retention: RecordRetention;
|
|
118
|
+
readonly deviceRemoval?: 'delete' | 'none';
|
|
119
|
+
readonly maxDataBytes?: number;
|
|
120
|
+
readonly secretArrayKeys?: Readonly<Record<string, string>>;
|
|
121
|
+
readonly adoptsFrom?: string;
|
|
122
|
+
readonly transactFamily?: string;
|
|
123
|
+
/** Makes this an ENTRY kind: it gets the generic list/add/edit/delete UI. */
|
|
124
|
+
readonly entry?: {
|
|
125
|
+
readonly title: string;
|
|
126
|
+
readonly nameField?: string;
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** A declared kind: its name, the row schema and the JSON the engine stores. */
|
|
130
|
+
export interface DefinedRecordKind<S extends z.ZodType = z.ZodType> {
|
|
131
|
+
readonly name: string;
|
|
132
|
+
readonly schema: S;
|
|
133
|
+
readonly declaration: RecordKindDeclaration;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Declare the kinds of a package. The keys are the kind names. The result is
|
|
137
|
+
* what `ctx.records.declare` sends to the engine and what `ctx.records.kind`
|
|
138
|
+
* types its handle from.
|
|
139
|
+
*/
|
|
140
|
+
export declare function defineRecordKinds<const T extends Readonly<Record<string, RecordKindSpec>>>(specs: T): {
|
|
141
|
+
readonly [K in keyof T & string]: DefinedRecordKind<T[K]['schema']>;
|
|
142
|
+
};
|
|
143
|
+
/** The wire form of a package's kinds: name -> declaration. */
|
|
144
|
+
export declare function declarationsOf(kinds: Readonly<Record<string, DefinedRecordKind>>): Readonly<Record<string, RecordKindDeclaration>>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ctx.records` - an addon's handle on its OWN record kinds (D881).
|
|
3
|
+
*
|
|
4
|
+
* The owner is closed over at construction (the addon id) and the transport
|
|
5
|
+
* decides how it is proven: a hub in-process caller states it, a forked or
|
|
6
|
+
* agent addon sends the bare input and the parent stamps who is calling - the
|
|
7
|
+
* `ctx.secrets` model. Nothing a caller passes can change it.
|
|
8
|
+
*
|
|
9
|
+
* ## Secrets are a feature of the kind
|
|
10
|
+
*
|
|
11
|
+
* A field of the kind's Zod schema marked `.meta({ secret: true })` is stored
|
|
12
|
+
* as a keychain MARKER bound to the row's own ref and never as a value:
|
|
13
|
+
*
|
|
14
|
+
* - **write**: a non-empty string goes to the keychain FIRST, then the row is
|
|
15
|
+
* written holding the marker. The redaction token (or an empty/absent
|
|
16
|
+
* value on a `patch`) keeps what is stored; `put` is a full replace, so a
|
|
17
|
+
* secret field it omits is removed - and so is its keychain value. A locked
|
|
18
|
+
* keychain refuses the whole write (`records/secret-unavailable`):
|
|
19
|
+
* plaintext is never stored, never logged, never in an error message.
|
|
20
|
+
* - **read**: every read shows the redaction token for a set secret. The
|
|
21
|
+
* owner that needs the value asks for it (`getResolved`, `query` with
|
|
22
|
+
* `resolveSecrets`); the keychain answering `unavailable` or `missing`
|
|
23
|
+
* THROWS - a locked keychain is never read as an empty password.
|
|
24
|
+
* - **delete**: the keychain values of the removed rows go with them.
|
|
25
|
+
*/
|
|
26
|
+
import type { z } from 'zod';
|
|
27
|
+
import type { DeclareKindResult, IRecordsProvider, RecordQueryInput, RecordWhere } from '../capabilities/records.cap.js';
|
|
28
|
+
import type { SecretsAccess } from '../keychain/secrets-access.js';
|
|
29
|
+
import { type DefinedRecordKind } from './record-kind.js';
|
|
30
|
+
import { collectSecretLeaves } from './secret-paths.js';
|
|
31
|
+
/** The wire: the `records` provider's methods. The owner is never an argument. */
|
|
32
|
+
export type RecordsTransport = IRecordsProvider;
|
|
33
|
+
/** A row as the owner sees it. Secret fields hold the redaction token unless resolved. */
|
|
34
|
+
export interface RecordRow<T> {
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly deviceId: number | null;
|
|
37
|
+
readonly at: number | null;
|
|
38
|
+
readonly expiresAt: number | null;
|
|
39
|
+
readonly rev: number;
|
|
40
|
+
readonly updatedAt: number;
|
|
41
|
+
readonly data: T;
|
|
42
|
+
}
|
|
43
|
+
export interface RecordPut<T> {
|
|
44
|
+
readonly id: string;
|
|
45
|
+
readonly deviceId?: number;
|
|
46
|
+
readonly at?: number;
|
|
47
|
+
readonly expiresAt?: number;
|
|
48
|
+
readonly data: T;
|
|
49
|
+
}
|
|
50
|
+
export interface RecordsLogger {
|
|
51
|
+
warn(message: string, extras?: {
|
|
52
|
+
readonly meta?: Record<string, unknown>;
|
|
53
|
+
}): void;
|
|
54
|
+
}
|
|
55
|
+
export interface RecordsAccessOptions {
|
|
56
|
+
readonly owner: string;
|
|
57
|
+
readonly transport: RecordsTransport;
|
|
58
|
+
/** The owner's keychain handle; `null` where there is none (a kind with secrets then refuses). */
|
|
59
|
+
readonly secrets: SecretsAccess | null;
|
|
60
|
+
readonly logger?: RecordsLogger;
|
|
61
|
+
/** Builds the transport for another owner - in-process system builtins only. */
|
|
62
|
+
readonly transportForOwner?: (owner: string) => RecordsTransport;
|
|
63
|
+
}
|
|
64
|
+
export interface RecordQueryOptions {
|
|
65
|
+
/** Resolve the secret fields of the page in ONE keychain call. A failure throws. */
|
|
66
|
+
readonly resolveSecrets?: boolean;
|
|
67
|
+
}
|
|
68
|
+
export interface RecordKindHandle<T> {
|
|
69
|
+
readonly name: string;
|
|
70
|
+
get(id: string): Promise<RecordRow<T> | null>;
|
|
71
|
+
getMany(ids: readonly string[]): Promise<readonly RecordRow<T>[]>;
|
|
72
|
+
/** `get` with the secret fields resolved from the keychain. */
|
|
73
|
+
getResolved(id: string): Promise<RecordRow<T> | null>;
|
|
74
|
+
put(input: RecordPut<T>, opts?: {
|
|
75
|
+
readonly ifRev?: number;
|
|
76
|
+
}): Promise<{
|
|
77
|
+
readonly rev: number;
|
|
78
|
+
}>;
|
|
79
|
+
putMany(inputs: readonly RecordPut<T>[]): Promise<{
|
|
80
|
+
readonly count: number;
|
|
81
|
+
}>;
|
|
82
|
+
/** RFC 7396 merge in one statement. `null` removes a field. */
|
|
83
|
+
patch(id: string, partial: Readonly<Record<string, unknown>>, opts?: {
|
|
84
|
+
readonly ifRev?: number;
|
|
85
|
+
readonly at?: number | null;
|
|
86
|
+
readonly expiresAt?: number | null;
|
|
87
|
+
}): Promise<{
|
|
88
|
+
readonly rev: number;
|
|
89
|
+
}>;
|
|
90
|
+
delete(id: string): Promise<boolean>;
|
|
91
|
+
deleteMany(ids: readonly string[]): Promise<number>;
|
|
92
|
+
query(q: Omit<RecordQueryInput, 'kind' | 'limit'> & {
|
|
93
|
+
readonly limit?: number;
|
|
94
|
+
}, opts?: RecordQueryOptions): Promise<{
|
|
95
|
+
readonly records: readonly RecordRow<T>[];
|
|
96
|
+
readonly nextCursor: string | null;
|
|
97
|
+
}>;
|
|
98
|
+
count(q: {
|
|
99
|
+
readonly deviceId?: number;
|
|
100
|
+
readonly atFrom?: number;
|
|
101
|
+
readonly atTo?: number;
|
|
102
|
+
readonly where?: RecordWhere;
|
|
103
|
+
}): Promise<number>;
|
|
104
|
+
histogram(q: {
|
|
105
|
+
readonly deviceId?: number;
|
|
106
|
+
readonly from: number;
|
|
107
|
+
readonly to: number;
|
|
108
|
+
readonly bucketMs: number;
|
|
109
|
+
}): Promise<readonly {
|
|
110
|
+
readonly bucketStart: number;
|
|
111
|
+
readonly count: number;
|
|
112
|
+
}[]>;
|
|
113
|
+
aggregate(q: {
|
|
114
|
+
readonly op: 'sum' | 'min' | 'max' | 'count';
|
|
115
|
+
readonly field?: string;
|
|
116
|
+
readonly groupBy?: string;
|
|
117
|
+
readonly where?: RecordWhere;
|
|
118
|
+
}): Promise<readonly {
|
|
119
|
+
readonly group: string | number | null;
|
|
120
|
+
readonly value: number | null;
|
|
121
|
+
}[]>;
|
|
122
|
+
deleteWhere(q: {
|
|
123
|
+
readonly deviceId?: number;
|
|
124
|
+
readonly atBefore?: number;
|
|
125
|
+
readonly where?: RecordWhere;
|
|
126
|
+
}): Promise<number>;
|
|
127
|
+
allocateId(): Promise<number>;
|
|
128
|
+
}
|
|
129
|
+
export interface RecordsAccess {
|
|
130
|
+
readonly owner: string;
|
|
131
|
+
/**
|
|
132
|
+
* Reconcile this addon's kinds (the engine creates the indexes; additive
|
|
133
|
+
* only). THROWS {@link AdoptionPendingError} while a kind is empty and the
|
|
134
|
+
* legacy table it adopts from still holds rows - the boot guard (D853: no
|
|
135
|
+
* migration code in the product).
|
|
136
|
+
*/
|
|
137
|
+
declare(kinds: Readonly<Record<string, DefinedRecordKind>>): Promise<Readonly<Record<string, DeclareKindResult>>>;
|
|
138
|
+
kind<S extends z.ZodType>(definition: DefinedRecordKind<S>): RecordKindHandle<z.infer<S>>;
|
|
139
|
+
/**
|
|
140
|
+
* In-process SYSTEM builtins only (the entries service): act as the door of
|
|
141
|
+
* another owner. `null` everywhere else - a forked addon has no way to state
|
|
142
|
+
* an identity the transport would accept.
|
|
143
|
+
*/
|
|
144
|
+
readonly forOwner: ((owner: string) => RecordsAccess) | null;
|
|
145
|
+
/** The D865 hook body: delete the device's rows from every kind declared `deviceRemoval: 'delete'`. */
|
|
146
|
+
deleteDevice(deviceId: number, opts?: {
|
|
147
|
+
readonly kinds?: readonly string[];
|
|
148
|
+
}): Promise<Readonly<Record<string, number>>>;
|
|
149
|
+
}
|
|
150
|
+
/** Build the owner's records handle. */
|
|
151
|
+
export declare function createRecordsAccess(options: RecordsAccessOptions): RecordsAccess;
|
|
152
|
+
/** Re-exported so adoption tooling builds the same concrete paths the facade does. */
|
|
153
|
+
export { collectSecretLeaves };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret fields of a record, addressed the way a Zod schema declares them
|
|
3
|
+
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
4
|
+
* (`auth.accessToken`, `headers[].value`).
|
|
5
|
+
*
|
|
6
|
+
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
7
|
+
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
8
|
+
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
9
|
+
* leaf names its element: `headers[X-Api].value`.
|
|
10
|
+
*
|
|
11
|
+
* Pure and browser-safe. Nothing here mutates: every transform returns a new value.
|
|
12
|
+
*/
|
|
13
|
+
import { arrayItemFieldPath } from '../keychain/secret-ref.js';
|
|
14
|
+
/** One segment of a declared path. */
|
|
15
|
+
export interface SecretPathSegment {
|
|
16
|
+
readonly key: string;
|
|
17
|
+
/** `true` when the key holds an array and the path continues into each element. */
|
|
18
|
+
readonly each: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** The array prefixes (declared form, e.g. `headers` or `a.headers`) of a path. */
|
|
21
|
+
export declare function parseSecretPath(path: string): readonly SecretPathSegment[];
|
|
22
|
+
/** The declared array prefixes of `path` that need an id key (`headers`, `a.rows`). */
|
|
23
|
+
export declare function secretArrayPrefixes(path: string): readonly string[];
|
|
24
|
+
/** One secret leaf found in a record body. */
|
|
25
|
+
export interface SecretLeaf {
|
|
26
|
+
/** The declared path it matched (`headers[].value`). */
|
|
27
|
+
readonly declared: string;
|
|
28
|
+
/** Its concrete path, naming array elements by id (`headers[X-Api].value`). */
|
|
29
|
+
readonly concrete: string;
|
|
30
|
+
readonly value: unknown;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
|
|
34
|
+
* visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
|
|
35
|
+
* result is a new body, `data` is untouched.
|
|
36
|
+
*/
|
|
37
|
+
export declare const REMOVE_LEAF: unique symbol;
|
|
38
|
+
export type LeafReplacement = unknown | typeof REMOVE_LEAF;
|
|
39
|
+
/** Replace/remove every present secret leaf; returns a NEW body. */
|
|
40
|
+
export declare function mapSecretLeaves(data: unknown, paths: readonly string[], arrayKeys: Readonly<Record<string, string>>, visit: (leaf: SecretLeaf) => LeafReplacement): unknown;
|
|
41
|
+
/** Every present secret leaf, read-only. */
|
|
42
|
+
export declare function collectSecretLeaves(data: unknown, paths: readonly string[], arrayKeys: Readonly<Record<string, string>>): readonly SecretLeaf[];
|
|
43
|
+
/**
|
|
44
|
+
* The keychain collection name a kind's secrets live under. Prefixed so a
|
|
45
|
+
* record's refs never collide with the refs a legacy collection of the same name
|
|
46
|
+
* already holds during an adoption.
|
|
47
|
+
*/
|
|
48
|
+
export declare function recordSecretCollection(kind: string): string;
|
|
49
|
+
/** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
|
|
50
|
+
export declare function recordSecretRef(owner: string, kind: string, rowId: string, concretePath: string): string;
|
|
51
|
+
/** A concrete path built from a flat array item, re-exported for adoption tooling. */
|
|
52
|
+
export declare const recordArrayItemPath: typeof arrayItemFieldPath;
|