@noy-db/core 0.1.0

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.
@@ -0,0 +1,526 @@
1
+ /** Format version for encrypted record envelopes. */
2
+ declare const NOYDB_FORMAT_VERSION: 1;
3
+ /** Format version for keyring files. */
4
+ declare const NOYDB_KEYRING_VERSION: 1;
5
+ /** Format version for backup files. */
6
+ declare const NOYDB_BACKUP_VERSION: 1;
7
+ /** Format version for sync metadata. */
8
+ declare const NOYDB_SYNC_VERSION: 1;
9
+ type Role = 'owner' | 'admin' | 'operator' | 'viewer' | 'client';
10
+ type Permission = 'rw' | 'ro';
11
+ type Permissions = Record<string, Permission>;
12
+ /** The encrypted wrapper stored by adapters. Adapters only ever see this. */
13
+ interface EncryptedEnvelope {
14
+ readonly _noydb: typeof NOYDB_FORMAT_VERSION;
15
+ readonly _v: number;
16
+ readonly _ts: string;
17
+ readonly _iv: string;
18
+ readonly _data: string;
19
+ /** User who created this version (unencrypted metadata). */
20
+ readonly _by?: string;
21
+ }
22
+ /** All records across all collections for a compartment. */
23
+ type CompartmentSnapshot = Record<string, Record<string, EncryptedEnvelope>>;
24
+ interface NoydbAdapter {
25
+ /** Get a single record. Returns null if not found. */
26
+ get(compartment: string, collection: string, id: string): Promise<EncryptedEnvelope | null>;
27
+ /** Put a record. Throws ConflictError if expectedVersion doesn't match. */
28
+ put(compartment: string, collection: string, id: string, envelope: EncryptedEnvelope, expectedVersion?: number): Promise<void>;
29
+ /** Delete a record. */
30
+ delete(compartment: string, collection: string, id: string): Promise<void>;
31
+ /** List all record IDs in a collection. */
32
+ list(compartment: string, collection: string): Promise<string[]>;
33
+ /** Load all records for a compartment (initial hydration). */
34
+ loadAll(compartment: string): Promise<CompartmentSnapshot>;
35
+ /** Save all records for a compartment (bulk write / restore). */
36
+ saveAll(compartment: string, data: CompartmentSnapshot): Promise<void>;
37
+ /** Optional connectivity check for sync engine. */
38
+ ping?(): Promise<boolean>;
39
+ }
40
+ /** Type-safe helper for creating adapter factories. */
41
+ declare function defineAdapter<TOptions>(factory: (options: TOptions) => NoydbAdapter): (options: TOptions) => NoydbAdapter;
42
+ interface KeyringFile {
43
+ readonly _noydb_keyring: typeof NOYDB_KEYRING_VERSION;
44
+ readonly user_id: string;
45
+ readonly display_name: string;
46
+ readonly role: Role;
47
+ readonly permissions: Permissions;
48
+ readonly deks: Record<string, string>;
49
+ readonly salt: string;
50
+ readonly created_at: string;
51
+ readonly granted_by: string;
52
+ }
53
+ interface CompartmentBackup {
54
+ readonly _noydb_backup: typeof NOYDB_BACKUP_VERSION;
55
+ readonly _compartment: string;
56
+ readonly _exported_at: string;
57
+ readonly _exported_by: string;
58
+ readonly keyrings: Record<string, KeyringFile>;
59
+ readonly collections: CompartmentSnapshot;
60
+ }
61
+ interface DirtyEntry {
62
+ readonly compartment: string;
63
+ readonly collection: string;
64
+ readonly id: string;
65
+ readonly action: 'put' | 'delete';
66
+ readonly version: number;
67
+ readonly timestamp: string;
68
+ }
69
+ interface SyncMetadata {
70
+ readonly _noydb_sync: typeof NOYDB_SYNC_VERSION;
71
+ readonly last_push: string | null;
72
+ readonly last_pull: string | null;
73
+ readonly dirty: DirtyEntry[];
74
+ }
75
+ interface Conflict {
76
+ readonly compartment: string;
77
+ readonly collection: string;
78
+ readonly id: string;
79
+ readonly local: EncryptedEnvelope;
80
+ readonly remote: EncryptedEnvelope;
81
+ readonly localVersion: number;
82
+ readonly remoteVersion: number;
83
+ }
84
+ type ConflictStrategy = 'local-wins' | 'remote-wins' | 'version' | ((conflict: Conflict) => 'local' | 'remote');
85
+ interface PushResult {
86
+ readonly pushed: number;
87
+ readonly conflicts: Conflict[];
88
+ readonly errors: Error[];
89
+ }
90
+ interface PullResult {
91
+ readonly pulled: number;
92
+ readonly conflicts: Conflict[];
93
+ readonly errors: Error[];
94
+ }
95
+ interface SyncStatus {
96
+ readonly dirty: number;
97
+ readonly lastPush: string | null;
98
+ readonly lastPull: string | null;
99
+ readonly online: boolean;
100
+ }
101
+ interface ChangeEvent {
102
+ readonly compartment: string;
103
+ readonly collection: string;
104
+ readonly id: string;
105
+ readonly action: 'put' | 'delete';
106
+ }
107
+ interface NoydbEventMap {
108
+ 'change': ChangeEvent;
109
+ 'error': Error;
110
+ 'sync:push': PushResult;
111
+ 'sync:pull': PullResult;
112
+ 'sync:conflict': Conflict;
113
+ 'sync:online': void;
114
+ 'sync:offline': void;
115
+ 'history:save': {
116
+ compartment: string;
117
+ collection: string;
118
+ id: string;
119
+ version: number;
120
+ };
121
+ 'history:prune': {
122
+ compartment: string;
123
+ collection: string;
124
+ id: string;
125
+ pruned: number;
126
+ };
127
+ }
128
+ interface GrantOptions {
129
+ readonly userId: string;
130
+ readonly displayName: string;
131
+ readonly role: Role;
132
+ readonly passphrase: string;
133
+ readonly permissions?: Permissions;
134
+ }
135
+ interface RevokeOptions {
136
+ readonly userId: string;
137
+ readonly rotateKeys?: boolean;
138
+ }
139
+ interface UserInfo {
140
+ readonly userId: string;
141
+ readonly displayName: string;
142
+ readonly role: Role;
143
+ readonly permissions: Permissions;
144
+ readonly createdAt: string;
145
+ readonly grantedBy: string;
146
+ }
147
+ interface NoydbOptions {
148
+ /** Primary adapter (local storage). */
149
+ readonly adapter: NoydbAdapter;
150
+ /** Optional remote adapter for sync. */
151
+ readonly sync?: NoydbAdapter;
152
+ /** User identifier. */
153
+ readonly user: string;
154
+ /** Passphrase for key derivation. Required unless encrypt is false. */
155
+ readonly secret?: string;
156
+ /** Auth method. Default: 'passphrase'. */
157
+ readonly auth?: 'passphrase' | 'biometric';
158
+ /** Enable encryption. Default: true. */
159
+ readonly encrypt?: boolean;
160
+ /** Conflict resolution strategy. Default: 'version'. */
161
+ readonly conflict?: ConflictStrategy;
162
+ /** Auto-sync on online/offline events. Default: false. */
163
+ readonly autoSync?: boolean;
164
+ /** Periodic sync interval in ms. Default: 30000. */
165
+ readonly syncInterval?: number;
166
+ /** Session timeout in ms. Clears keys after inactivity. Default: none. */
167
+ readonly sessionTimeout?: number;
168
+ /** Validate passphrase strength on creation. Default: true. */
169
+ readonly validatePassphrase?: boolean;
170
+ /** Audit history configuration. */
171
+ readonly history?: HistoryConfig;
172
+ }
173
+ /** History configuration. */
174
+ interface HistoryConfig {
175
+ /** Enable history tracking. Default: true. */
176
+ readonly enabled?: boolean;
177
+ /** Maximum history entries per record. Oldest pruned on overflow. Default: unlimited. */
178
+ readonly maxVersions?: number;
179
+ }
180
+ /** Options for querying history. */
181
+ interface HistoryOptions {
182
+ /** Start date (inclusive), ISO 8601. */
183
+ readonly from?: string;
184
+ /** End date (inclusive), ISO 8601. */
185
+ readonly to?: string;
186
+ /** Maximum entries to return. */
187
+ readonly limit?: number;
188
+ }
189
+ /** Options for pruning history. */
190
+ interface PruneOptions {
191
+ /** Keep only the N most recent versions. */
192
+ readonly keepVersions?: number;
193
+ /** Delete versions older than this date, ISO 8601. */
194
+ readonly beforeDate?: string;
195
+ }
196
+ /** A decrypted history entry. */
197
+ interface HistoryEntry<T> {
198
+ readonly version: number;
199
+ readonly timestamp: string;
200
+ readonly userId: string;
201
+ readonly record: T;
202
+ }
203
+
204
+ declare class NoydbError extends Error {
205
+ readonly code: string;
206
+ constructor(code: string, message: string);
207
+ }
208
+ declare class DecryptionError extends NoydbError {
209
+ constructor(message?: string);
210
+ }
211
+ declare class TamperedError extends NoydbError {
212
+ constructor(message?: string);
213
+ }
214
+ declare class InvalidKeyError extends NoydbError {
215
+ constructor(message?: string);
216
+ }
217
+ declare class NoAccessError extends NoydbError {
218
+ constructor(message?: string);
219
+ }
220
+ declare class ReadOnlyError extends NoydbError {
221
+ constructor(message?: string);
222
+ }
223
+ declare class PermissionDeniedError extends NoydbError {
224
+ constructor(message?: string);
225
+ }
226
+ declare class ConflictError extends NoydbError {
227
+ readonly version: number;
228
+ constructor(version: number, message?: string);
229
+ }
230
+ declare class NetworkError extends NoydbError {
231
+ constructor(message?: string);
232
+ }
233
+ declare class NotFoundError extends NoydbError {
234
+ constructor(message?: string);
235
+ }
236
+ declare class ValidationError extends NoydbError {
237
+ constructor(message?: string);
238
+ }
239
+
240
+ /** In-memory representation of an unlocked keyring. */
241
+ interface UnlockedKeyring {
242
+ readonly userId: string;
243
+ readonly displayName: string;
244
+ readonly role: Role;
245
+ readonly permissions: Permissions;
246
+ readonly deks: Map<string, CryptoKey>;
247
+ readonly kek: CryptoKey;
248
+ readonly salt: Uint8Array;
249
+ }
250
+
251
+ type EventHandler<T> = (data: T) => void;
252
+ /** Typed event emitter for NOYDB events. */
253
+ declare class NoydbEventEmitter {
254
+ private readonly listeners;
255
+ on<K extends keyof NoydbEventMap>(event: K, handler: EventHandler<NoydbEventMap[K]>): void;
256
+ off<K extends keyof NoydbEventMap>(event: K, handler: EventHandler<NoydbEventMap[K]>): void;
257
+ emit<K extends keyof NoydbEventMap>(event: K, data: NoydbEventMap[K]): void;
258
+ removeAllListeners(): void;
259
+ }
260
+
261
+ /**
262
+ * Zero-dependency JSON diff.
263
+ * Produces a flat list of changes between two plain objects.
264
+ */
265
+ type ChangeType = 'added' | 'removed' | 'changed';
266
+ interface DiffEntry {
267
+ /** Dot-separated path to the changed field (e.g. "address.city"). */
268
+ readonly path: string;
269
+ /** Type of change. */
270
+ readonly type: ChangeType;
271
+ /** Previous value (undefined for 'added'). */
272
+ readonly from?: unknown;
273
+ /** New value (undefined for 'removed'). */
274
+ readonly to?: unknown;
275
+ }
276
+ /**
277
+ * Compute differences between two objects.
278
+ * Returns an array of DiffEntry describing each changed field.
279
+ * Returns empty array if objects are identical.
280
+ */
281
+ declare function diff(oldObj: unknown, newObj: unknown, basePath?: string): DiffEntry[];
282
+ /** Format a diff as a human-readable string. */
283
+ declare function formatDiff(changes: DiffEntry[]): string;
284
+
285
+ /** Callback for dirty tracking (sync engine integration). */
286
+ type OnDirtyCallback = (collection: string, id: string, action: 'put' | 'delete', version: number) => Promise<void>;
287
+ /** A typed collection of records within a compartment. */
288
+ declare class Collection<T> {
289
+ private readonly adapter;
290
+ private readonly compartment;
291
+ private readonly name;
292
+ private readonly keyring;
293
+ private readonly encrypted;
294
+ private readonly emitter;
295
+ private readonly getDEK;
296
+ private readonly onDirty;
297
+ private readonly historyConfig;
298
+ private readonly cache;
299
+ private hydrated;
300
+ constructor(opts: {
301
+ adapter: NoydbAdapter;
302
+ compartment: string;
303
+ name: string;
304
+ keyring: UnlockedKeyring;
305
+ encrypted: boolean;
306
+ emitter: NoydbEventEmitter;
307
+ getDEK: (collectionName: string) => Promise<CryptoKey>;
308
+ historyConfig?: HistoryConfig | undefined;
309
+ onDirty?: OnDirtyCallback | undefined;
310
+ });
311
+ /** Get a single record by ID. Returns null if not found. */
312
+ get(id: string): Promise<T | null>;
313
+ /** Create or update a record. */
314
+ put(id: string, record: T): Promise<void>;
315
+ /** Delete a record by ID. */
316
+ delete(id: string): Promise<void>;
317
+ /** List all records in the collection. */
318
+ list(): Promise<T[]>;
319
+ /** Filter records by a predicate. */
320
+ query(predicate: (record: T) => boolean): T[];
321
+ /** Get version history for a record, newest first. */
322
+ history(id: string, options?: HistoryOptions): Promise<HistoryEntry<T>[]>;
323
+ /** Get a specific past version of a record. */
324
+ getVersion(id: string, version: number): Promise<T | null>;
325
+ /** Revert a record to a past version. Creates a new version with the old content. */
326
+ revert(id: string, version: number): Promise<void>;
327
+ /**
328
+ * Compare two versions of a record and return the differences.
329
+ * Use version 0 to represent "before creation" (empty).
330
+ * Omit versionB to compare against the current version.
331
+ */
332
+ diff(id: string, versionA: number, versionB?: number): Promise<DiffEntry[]>;
333
+ /** Resolve a version: try history first, then check if it's the current version. */
334
+ private resolveVersion;
335
+ private resolveCurrentOrVersion;
336
+ /** Prune history entries for a record (or all records if id is undefined). */
337
+ pruneRecordHistory(id: string | undefined, options: PruneOptions): Promise<number>;
338
+ /** Clear all history for this collection (or a specific record). */
339
+ clearHistory(id?: string): Promise<number>;
340
+ /** Count records in the collection. */
341
+ count(): Promise<number>;
342
+ /** Load all records from adapter into memory cache. */
343
+ private ensureHydrated;
344
+ /** Hydrate from a pre-loaded snapshot (used by Compartment). */
345
+ hydrateFromSnapshot(records: Record<string, EncryptedEnvelope>): Promise<void>;
346
+ /** Get all records as encrypted envelopes (for dump). */
347
+ dumpEnvelopes(): Promise<Record<string, EncryptedEnvelope>>;
348
+ private encryptRecord;
349
+ private decryptRecord;
350
+ }
351
+
352
+ /** A compartment (tenant namespace) containing collections. */
353
+ declare class Compartment {
354
+ private readonly adapter;
355
+ private readonly name;
356
+ private readonly keyring;
357
+ private readonly encrypted;
358
+ private readonly emitter;
359
+ private readonly onDirty;
360
+ private readonly historyConfig;
361
+ private readonly getDEK;
362
+ private readonly collectionCache;
363
+ constructor(opts: {
364
+ adapter: NoydbAdapter;
365
+ name: string;
366
+ keyring: UnlockedKeyring;
367
+ encrypted: boolean;
368
+ emitter: NoydbEventEmitter;
369
+ onDirty?: OnDirtyCallback | undefined;
370
+ historyConfig?: HistoryConfig | undefined;
371
+ });
372
+ /** Open a typed collection within this compartment. */
373
+ collection<T>(collectionName: string): Collection<T>;
374
+ /** List all collection names in this compartment. */
375
+ collections(): Promise<string[]>;
376
+ /** Dump compartment as encrypted JSON backup string. */
377
+ dump(): Promise<string>;
378
+ /** Restore compartment from an encrypted JSON backup string. */
379
+ load(backupJson: string): Promise<void>;
380
+ /** Export compartment as decrypted JSON (owner only). */
381
+ export(): Promise<string>;
382
+ }
383
+
384
+ /** The top-level NOYDB instance. */
385
+ declare class Noydb {
386
+ private readonly options;
387
+ private readonly emitter;
388
+ private readonly compartmentCache;
389
+ private readonly keyringCache;
390
+ private readonly syncEngines;
391
+ private closed;
392
+ private sessionTimer;
393
+ constructor(options: NoydbOptions);
394
+ private resetSessionTimer;
395
+ /** Open a compartment by name. */
396
+ openCompartment(name: string): Promise<Compartment>;
397
+ /** Synchronous compartment access (must call openCompartment first, or auto-opens). */
398
+ compartment(name: string): Compartment;
399
+ /** Grant access to a user for a compartment. */
400
+ grant(compartment: string, options: GrantOptions): Promise<void>;
401
+ /** Revoke a user's access to a compartment. */
402
+ revoke(compartment: string, options: RevokeOptions): Promise<void>;
403
+ /** List all users with access to a compartment. */
404
+ listUsers(compartment: string): Promise<UserInfo[]>;
405
+ /** Change the current user's passphrase for a compartment. */
406
+ changeSecret(compartment: string, newPassphrase: string): Promise<void>;
407
+ /** Push local changes to remote for a compartment. */
408
+ push(compartment: string): Promise<PushResult>;
409
+ /** Pull remote changes to local for a compartment. */
410
+ pull(compartment: string): Promise<PullResult>;
411
+ /** Bidirectional sync: pull then push. */
412
+ sync(compartment: string): Promise<{
413
+ pull: PullResult;
414
+ push: PushResult;
415
+ }>;
416
+ /** Get sync status for a compartment. */
417
+ syncStatus(compartment: string): SyncStatus;
418
+ private getSyncEngine;
419
+ on<K extends keyof NoydbEventMap>(event: K, handler: (data: NoydbEventMap[K]) => void): void;
420
+ off<K extends keyof NoydbEventMap>(event: K, handler: (data: NoydbEventMap[K]) => void): void;
421
+ close(): void;
422
+ /** Get or load the keyring for a compartment. */
423
+ private getKeyring;
424
+ }
425
+ /** Create a new NOYDB instance. */
426
+ declare function createNoydb(options: NoydbOptions): Promise<Noydb>;
427
+
428
+ /** Sync engine: dirty tracking, push, pull, conflict resolution. */
429
+ declare class SyncEngine {
430
+ private readonly local;
431
+ private readonly remote;
432
+ private readonly strategy;
433
+ private readonly emitter;
434
+ private readonly compartment;
435
+ private dirty;
436
+ private lastPush;
437
+ private lastPull;
438
+ private loaded;
439
+ private autoSyncInterval;
440
+ private isOnline;
441
+ constructor(opts: {
442
+ local: NoydbAdapter;
443
+ remote: NoydbAdapter;
444
+ compartment: string;
445
+ strategy: ConflictStrategy;
446
+ emitter: NoydbEventEmitter;
447
+ });
448
+ /** Record a local change for later push. */
449
+ trackChange(collection: string, id: string, action: 'put' | 'delete', version: number): Promise<void>;
450
+ /** Push dirty records to remote adapter. */
451
+ push(): Promise<PushResult>;
452
+ /** Pull remote records to local adapter. */
453
+ pull(): Promise<PullResult>;
454
+ /** Bidirectional sync: pull then push. */
455
+ sync(): Promise<{
456
+ pull: PullResult;
457
+ push: PushResult;
458
+ }>;
459
+ /** Get current sync status. */
460
+ status(): SyncStatus;
461
+ /** Start auto-sync: listen for online/offline events, optional periodic sync. */
462
+ startAutoSync(intervalMs?: number): void;
463
+ /** Stop auto-sync. */
464
+ stopAutoSync(): void;
465
+ private handleOnline;
466
+ private handleOffline;
467
+ /** Resolve a conflict using the configured strategy. */
468
+ private resolveConflict;
469
+ private ensureLoaded;
470
+ private persistMeta;
471
+ }
472
+
473
+ /**
474
+ * WebAuthn biometric enrollment and unlock.
475
+ *
476
+ * Enrollment: User enters passphrase → derive KEK → create WebAuthn credential
477
+ * → wrap KEK with credential-derived key → store { credentialId, wrappedKek }
478
+ *
479
+ * Unlock: Retrieve { credentialId, wrappedKek } → WebAuthn assertion
480
+ * → unwrap KEK → proceed as passphrase auth
481
+ *
482
+ * This module requires a browser environment with WebAuthn support.
483
+ */
484
+ interface BiometricCredential {
485
+ credentialId: string;
486
+ wrappedKek: string;
487
+ salt: string;
488
+ }
489
+ /** Check if WebAuthn is available in the current environment. */
490
+ declare function isBiometricAvailable(): boolean;
491
+ /**
492
+ * Enroll a biometric credential for the current user.
493
+ * Must be called after passphrase authentication (KEK is in memory).
494
+ *
495
+ * @param userId - User identifier for WebAuthn
496
+ * @param kek - The KEK derived from the user's passphrase (in memory)
497
+ * @returns BiometricCredential to persist in browser storage
498
+ */
499
+ declare function enrollBiometric(userId: string, kek: CryptoKey): Promise<BiometricCredential>;
500
+ /**
501
+ * Unlock using a previously enrolled biometric credential.
502
+ *
503
+ * @param storedCredential - The stored BiometricCredential from enrollment
504
+ * @returns The unwrapped KEK as a CryptoKey
505
+ */
506
+ declare function unlockBiometric(storedCredential: BiometricCredential): Promise<CryptoKey>;
507
+ /** Remove biometric enrollment from browser storage. */
508
+ declare function removeBiometric(storage: Storage, userId: string): void;
509
+ /** Save biometric credential to browser storage. */
510
+ declare function saveBiometric(storage: Storage, userId: string, credential: BiometricCredential): void;
511
+ /** Load biometric credential from browser storage. */
512
+ declare function loadBiometric(storage: Storage, userId: string): BiometricCredential | null;
513
+
514
+ /**
515
+ * Validate passphrase strength.
516
+ * Checks length and basic entropy heuristics.
517
+ * Throws ValidationError if too weak.
518
+ */
519
+ declare function validatePassphrase(passphrase: string): void;
520
+ /**
521
+ * Estimate passphrase entropy in bits.
522
+ * Uses character class analysis (not dictionary-based).
523
+ */
524
+ declare function estimateEntropy(passphrase: string): number;
525
+
526
+ export { type BiometricCredential, type ChangeEvent, type ChangeType, Collection, Compartment, type CompartmentBackup, type CompartmentSnapshot, type Conflict, ConflictError, type ConflictStrategy, DecryptionError, type DiffEntry, type DirtyEntry, type EncryptedEnvelope, type GrantOptions, type HistoryConfig, type HistoryEntry, type HistoryOptions, InvalidKeyError, type KeyringFile, NOYDB_BACKUP_VERSION, NOYDB_FORMAT_VERSION, NOYDB_KEYRING_VERSION, NOYDB_SYNC_VERSION, NetworkError, NoAccessError, NotFoundError, Noydb, type NoydbAdapter, NoydbError, type NoydbEventMap, type NoydbOptions, type Permission, PermissionDeniedError, type Permissions, type PruneOptions, type PullResult, type PushResult, ReadOnlyError, type RevokeOptions, type Role, SyncEngine, type SyncMetadata, type SyncStatus, TamperedError, type UserInfo, ValidationError, createNoydb, defineAdapter, diff, enrollBiometric, estimateEntropy, formatDiff, isBiometricAvailable, loadBiometric, removeBiometric, saveBiometric, unlockBiometric, validatePassphrase };