@camstack/types 1.2.378 → 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.
Files changed (36) hide show
  1. package/dist/addon.d.ts +1 -0
  2. package/dist/addon.js +57 -2
  3. package/dist/addon.mjs +5 -3
  4. package/dist/capabilities/caller-stamp.d.ts +12 -0
  5. package/dist/capabilities/core-blocks.cap.d.ts +26 -4
  6. package/dist/capabilities/index.d.ts +7 -2
  7. package/dist/capabilities/llm.cap.d.ts +1 -0
  8. package/dist/capabilities/raw-secret-migration.d.ts +1 -0
  9. package/dist/capabilities/record-entries.cap.d.ts +84 -0
  10. package/dist/capabilities/records.cap.d.ts +399 -0
  11. package/dist/capabilities/storage-admin.cap.d.ts +246 -0
  12. package/dist/capabilities/user-management.cap.d.ts +1 -0
  13. package/dist/client.js +7 -0
  14. package/dist/client.mjs +7 -0
  15. package/dist/generated/addon-api.d.ts +184 -0
  16. package/dist/generated/cap-input-defaults.d.ts +1 -1
  17. package/dist/generated/capability-router-map.d.ts +8 -2
  18. package/dist/generated/method-access-map.d.ts +1 -1
  19. package/dist/generated/method-device-selectors.d.ts +2 -2
  20. package/dist/generated/system-proxy.d.ts +3 -1
  21. package/dist/index.d.ts +2 -1
  22. package/dist/index.js +746 -38
  23. package/dist/index.mjs +677 -39
  24. package/dist/interfaces/addon.d.ts +13 -0
  25. package/dist/interfaces/event-bus.d.ts +32 -3
  26. package/dist/interfaces/storage.d.ts +1 -1
  27. package/dist/records/constants.d.ts +23 -0
  28. package/dist/records/entry-kind.d.ts +31 -0
  29. package/dist/records/errors.d.ts +81 -0
  30. package/dist/records/index.d.ts +6 -0
  31. package/dist/records/record-kind.d.ts +144 -0
  32. package/dist/records/records-access.d.ts +153 -0
  33. package/dist/records/secret-paths.d.ts +52 -0
  34. package/dist/{sleep-gxUz0C9M.js → sleep-CIGOw58l.js} +1520 -309
  35. package/dist/{sleep-bgZLodYc.mjs → sleep-WsrozGDL.mjs} +999 -112
  36. 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
- readonly camStreamId: string;
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
- readonly camStreamId: string;
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' | 'provider-settings' | 'device-settings' | 'detection-events' | 'audio-levels' | 'track-trails' | 'recording-segments' | 'recording-thumbnails' | 'recording-policies' | 'recording-storage-config' | 'recording-cleanup-queue' | 'known-faces' | (string & {});
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,6 @@
1
+ export * from './constants.js';
2
+ export * from './errors.js';
3
+ export * from './record-kind.js';
4
+ export * from './secret-paths.js';
5
+ export * from './records-access.js';
6
+ export * from './entry-kind.js';
@@ -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;