local-lite 0.0.19 → 0.0.20

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 (63) hide show
  1. package/README.md +5 -1
  2. package/dist/actions/store.js +1 -1
  3. package/dist/core/client.d.ts +15 -0
  4. package/dist/core/client.js +46 -2
  5. package/dist/index.d.ts +5 -0
  6. package/dist/index.js +5 -0
  7. package/dist/keys/generator.d.ts +38 -0
  8. package/dist/keys/generator.js +136 -0
  9. package/dist/keys/index.d.ts +5 -0
  10. package/dist/keys/index.js +21 -0
  11. package/dist/keys/manager.d.ts +59 -0
  12. package/dist/keys/manager.js +127 -0
  13. package/dist/keys/signed-token.d.ts +18 -0
  14. package/dist/keys/signed-token.js +171 -0
  15. package/dist/keys/store.d.ts +23 -0
  16. package/dist/keys/store.js +109 -0
  17. package/dist/keys/types.d.ts +95 -0
  18. package/dist/keys/types.js +2 -0
  19. package/dist/kv/index.d.ts +3 -0
  20. package/dist/kv/index.js +19 -0
  21. package/dist/kv/manager.d.ts +52 -0
  22. package/dist/kv/manager.js +164 -0
  23. package/dist/kv/store.d.ts +13 -0
  24. package/dist/kv/store.js +85 -0
  25. package/dist/kv/types.d.ts +27 -0
  26. package/dist/kv/types.js +2 -0
  27. package/dist/react/index.d.ts +2 -0
  28. package/dist/react/index.js +2 -0
  29. package/dist/react/kv.d.ts +9 -0
  30. package/dist/react/kv.js +53 -0
  31. package/dist/react/search.d.ts +19 -0
  32. package/dist/react/search.js +49 -0
  33. package/dist/reactive/event-emitter.d.ts +1 -0
  34. package/dist/reactive/event-emitter.js +9 -6
  35. package/dist/scheduler/cron.d.ts +6 -0
  36. package/dist/scheduler/cron.js +62 -0
  37. package/dist/scheduler/index.d.ts +4 -0
  38. package/dist/scheduler/index.js +20 -0
  39. package/dist/scheduler/manager.d.ts +59 -0
  40. package/dist/scheduler/manager.js +186 -0
  41. package/dist/scheduler/store.d.ts +20 -0
  42. package/dist/scheduler/store.js +134 -0
  43. package/dist/scheduler/types.d.ts +44 -0
  44. package/dist/scheduler/types.js +2 -0
  45. package/dist/search/index.d.ts +3 -0
  46. package/dist/search/index.js +19 -0
  47. package/dist/search/manager.d.ts +42 -0
  48. package/dist/search/manager.js +123 -0
  49. package/dist/search/store.d.ts +33 -0
  50. package/dist/search/store.js +137 -0
  51. package/dist/search/types.d.ts +49 -0
  52. package/dist/search/types.js +2 -0
  53. package/dist/storage/driver.d.ts +25 -0
  54. package/dist/storage/driver.js +92 -0
  55. package/dist/storage/index.d.ts +4 -0
  56. package/dist/storage/index.js +20 -0
  57. package/dist/storage/manager.d.ts +48 -0
  58. package/dist/storage/manager.js +149 -0
  59. package/dist/storage/store.d.ts +14 -0
  60. package/dist/storage/store.js +116 -0
  61. package/dist/storage/types.d.ts +44 -0
  62. package/dist/storage/types.js +2 -0
  63. package/package.json +26 -1
package/README.md CHANGED
@@ -92,7 +92,11 @@ This recovers sync state; it does not repair corrupt SQLite files or cancel hung
92
92
  * **🔄 Infinite-Loop Safe:** Incoming remote deltas update local tables and fire reactive UI listeners without echoing back into the outbox.
93
93
  * **☁️ Database Backups:** Export and restore SQLite snapshots independently of your live sync backend.
94
94
  * **🔑 Google Sign-In Out of the Box:** Secure, zero-backend Google OAuth 2.0 (PKCE) authentication with automatic token refresh, user profile retrieval, persistent session storage, and `useGoogleAuth()` hook.
95
- * **📨 Action Outbox & Email Queue:** Enqueue transactional emails, push notifications, and webhooks offline with idempotency keys, automatic retry backoff, crash recovery, and TTL expiration.
95
+ * **📨 Action Outbox & Email Queue:** Enqueue transactional emails, push notifications, and webhooks offline with idempotency keys, automatic retry backoff, crash recovery, and TTL expiration.
96
+ * **⚡ Fast Key-Value & Encrypted Preferences (`client.kv`):** Typed storage, TTL expiration, and AES-256-GCM zero-knowledge secret protection.
97
+ * **🔍 Instant Full-Text Search (`client.search`):** SQLite FTS5 indexer with prefix matching, BM25 rank scoring, and highlighting.
98
+ * **📁 Offline Media & Blob Storage (`client.storage`):** Decoupled binary attachments, SHA-256 verification, and cloud sync queue (S3/Supabase/Firebase).
99
+ * **⏰ Offline Job Scheduler & Background Cron (`client.scheduler`):** Persistent background task runner supporting human schedules and 5-field cron.
96
100
 
97
101
 
98
102
  ---
@@ -169,7 +169,7 @@ class ActionStore {
169
169
  * Cancels a pending action if it has not yet executed.
170
170
  */
171
171
  static async cancel(db, tableName = exports.DEFAULT_ACTIONS_TABLE, id) {
172
- const rows = await db.execute(`UPDATE ${tableName} SET status = 'CANCELLED' WHERE id = ? AND status = 'PENDING';`, [id]);
172
+ await db.execute(`UPDATE ${tableName} SET status = 'CANCELLED' WHERE id = ? AND status = 'PENDING';`, [id]);
173
173
  // In drivers that return affected rows or execute successfully
174
174
  const record = await this.findById(db, tableName, id);
175
175
  return record?.status === 'CANCELLED';
@@ -10,6 +10,11 @@ import { QueryBuilder, AggregateSpec, AggregateResult } from './query-builder.js
10
10
  import { type ExportJsonOptions, type ImportJsonOptions, type ExportCsvOptions, type ImportCsvOptions, type ImportSummary } from '../utils/portability.js';
11
11
  import { type BackupStorage, type CreateDatabaseBackupOptions, type EncryptedBackupOptions, type RestoreEncryptedBackupOptions } from '../backups/index.js';
12
12
  import { ActionManager, type ActionManagerOptions } from '../actions/index.js';
13
+ import { ApiKeyManager, type ApiKeyManagerOptions } from '../keys/index.js';
14
+ import { KVManager, type KVStoreOptions } from '../kv/index.js';
15
+ import { SearchManager } from '../search/index.js';
16
+ import { BlobStorageManager, type BlobStorageManagerOptions } from '../storage/index.js';
17
+ import { JobScheduler, type JobSchedulerOptions } from '../scheduler/index.js';
13
18
  export interface LocalFirstClientConfig {
14
19
  local: LocalDBAdapter;
15
20
  remote: RemoteDBAdapter;
@@ -41,6 +46,10 @@ export interface LocalFirstClientConfig {
41
46
  */
42
47
  touchUpdatedAt?: boolean | string;
43
48
  actions?: ActionManagerOptions;
49
+ keys?: ApiKeyManagerOptions;
50
+ kv?: KVStoreOptions;
51
+ storage?: BlobStorageManagerOptions;
52
+ scheduler?: JobSchedulerOptions;
44
53
  }
45
54
  export declare class TransactionalClient {
46
55
  private parent;
@@ -89,6 +98,11 @@ export declare class LocalFirstClient {
89
98
  touchColumn: string | null;
90
99
  readonly maintenance: DatabaseMaintenance;
91
100
  readonly actions: ActionManager;
101
+ readonly keys: ApiKeyManager;
102
+ readonly kv: KVManager;
103
+ readonly search: SearchManager;
104
+ readonly storage: BlobStorageManager;
105
+ readonly scheduler: JobScheduler;
92
106
  private primaryKey;
93
107
  private migrations;
94
108
  private autoStart;
@@ -328,6 +342,7 @@ export declare class LocalFirstClient {
328
342
  */
329
343
  reset(options?: {
330
344
  dropTables?: boolean;
345
+ includeInternal?: boolean;
331
346
  }): Promise<void>;
332
347
  private getAllTableNames;
333
348
  /**
@@ -15,6 +15,11 @@ const query_builder_js_1 = require("./query-builder.js");
15
15
  const portability_js_1 = require("../utils/portability.js");
16
16
  const index_js_1 = require("../backups/index.js");
17
17
  const index_js_2 = require("../actions/index.js");
18
+ const index_js_3 = require("../keys/index.js");
19
+ const index_js_4 = require("../kv/index.js");
20
+ const index_js_5 = require("../search/index.js");
21
+ const index_js_6 = require("../storage/index.js");
22
+ const index_js_7 = require("../scheduler/index.js");
18
23
  function hasFunctionFilter(value) {
19
24
  if (typeof value === 'function')
20
25
  return true;
@@ -168,6 +173,11 @@ class LocalFirstClient {
168
173
  touchColumn;
169
174
  maintenance;
170
175
  actions;
176
+ keys;
177
+ kv;
178
+ search;
179
+ storage;
180
+ scheduler;
171
181
  primaryKey;
172
182
  migrations;
173
183
  autoStart;
@@ -192,8 +202,24 @@ class LocalFirstClient {
192
202
  ? touch
193
203
  : this.remote?.updatedAtColumn || 'updated_at';
194
204
  this.schemaManager = new schema_manager_js_1.SchemaManager();
195
- this.maintenance = new maintenance_js_1.DatabaseMaintenance(this.db, () => this.tables);
205
+ this.maintenance = new maintenance_js_1.DatabaseMaintenance(this.db, () => [
206
+ ...this.tables,
207
+ '_local_kv',
208
+ '_local_blobs',
209
+ '_local_scheduled_jobs',
210
+ '_local_job_runs',
211
+ '_local_api_keys',
212
+ '_local_actions',
213
+ ]);
196
214
  const actionNetwork = config.actions?.network ?? config.network;
215
+ this.keys = new index_js_3.ApiKeyManager(this.db, config.keys);
216
+ this.kv = new index_js_4.KVManager(this.db, this.events, config.kv);
217
+ this.search = new index_js_5.SearchManager(this.db, this.events, (table, id) => this.findById(table, id));
218
+ this.storage = new index_js_6.BlobStorageManager(this.db, {
219
+ network: config.network,
220
+ ...config.storage,
221
+ });
222
+ this.scheduler = new index_js_7.JobScheduler(this.db, config.scheduler);
197
223
  this.actions = new index_js_2.ActionManager(this.db, this.events, () => Promise.resolve(actionNetwork ? actionNetwork.isOnline() : true), {
198
224
  network: actionNetwork,
199
225
  ...config.actions,
@@ -357,6 +383,10 @@ class LocalFirstClient {
357
383
  await meta_store_js_1.MetaStore.ensureSchema(this.db);
358
384
  await outbox_store_js_1.OutboxStore.resetStaleProcessing(this.db);
359
385
  await this.actions.initialize();
386
+ await this.keys.initialize();
387
+ await this.kv.initialize();
388
+ await this.storage.initialize();
389
+ await this.scheduler.initialize();
360
390
  if (this.migrations.length > 0) {
361
391
  await this.db.transaction(async (tx) => {
362
392
  for (const query of this.migrations) {
@@ -871,6 +901,7 @@ class LocalFirstClient {
871
901
  stop() {
872
902
  this.syncWorker.stop();
873
903
  this.actions.stop();
904
+ this.scheduler.stop();
874
905
  }
875
906
  /**
876
907
  * Stops background sync workers, cleans up remote listeners,
@@ -891,7 +922,20 @@ class LocalFirstClient {
891
922
  */
892
923
  async reset(options = {}) {
893
924
  this.stop();
894
- const tablesToReset = Array.from(new Set([...this.tables, 'sync_outbox', 'sync_meta']));
925
+ const internalTables = [
926
+ '_local_kv',
927
+ '_local_blobs',
928
+ '_local_scheduled_jobs',
929
+ '_local_job_runs',
930
+ '_local_api_keys',
931
+ '_local_actions',
932
+ ];
933
+ const tablesToReset = Array.from(new Set([
934
+ ...this.tables,
935
+ 'sync_outbox',
936
+ 'sync_meta',
937
+ ...(options.includeInternal ? internalTables : []),
938
+ ]));
895
939
  for (const table of tablesToReset) {
896
940
  try {
897
941
  if (options.dropTables) {
package/dist/index.d.ts CHANGED
@@ -23,3 +23,8 @@ export * from './schema/index.js';
23
23
  export * from './auth/index.js';
24
24
  export * from './backups/index.js';
25
25
  export * from './actions/index.js';
26
+ export * from './keys/index.js';
27
+ export * from './kv/index.js';
28
+ export * from './search/index.js';
29
+ export * from './storage/index.js';
30
+ export * from './scheduler/index.js';
package/dist/index.js CHANGED
@@ -39,3 +39,8 @@ __exportStar(require("./schema/index.js"), exports);
39
39
  __exportStar(require("./auth/index.js"), exports);
40
40
  __exportStar(require("./backups/index.js"), exports);
41
41
  __exportStar(require("./actions/index.js"), exports);
42
+ __exportStar(require("./keys/index.js"), exports);
43
+ __exportStar(require("./kv/index.js"), exports);
44
+ __exportStar(require("./search/index.js"), exports);
45
+ __exportStar(require("./storage/index.js"), exports);
46
+ __exportStar(require("./scheduler/index.js"), exports);
@@ -0,0 +1,38 @@
1
+ import { GenerateApiKeyOptions, GeneratedApiKey } from './types.js';
2
+ /**
3
+ * Generates cryptographically secure random bytes across Web, Expo/React Native, and Node.js.
4
+ */
5
+ export declare function getRandomBytes(length: number): Uint8Array;
6
+ /**
7
+ * Encodes raw bytes into a base62 string.
8
+ */
9
+ export declare function bytesToBase62(bytes: Uint8Array): string;
10
+ /**
11
+ * Calculates a 4-byte (8-char hex) checksum over the prefix and random entropy.
12
+ */
13
+ export declare function calculateKeyChecksum(content: string): string;
14
+ /**
15
+ * Constant-time string comparison to prevent timing attacks.
16
+ */
17
+ export declare function timingSafeEqual(a: string, b: string): boolean;
18
+ /**
19
+ * Hashes an API key using SHA-256 for secure storage.
20
+ */
21
+ export declare function hashApiKey(key: string): string;
22
+ /**
23
+ * Generates an industry-standard, cryptographically secure API key.
24
+ * Format: [prefix]_[entropy]_[checksum]
25
+ *
26
+ * Example:
27
+ * prop_a7F9x2K1m8P0qW4z_c4f910e2
28
+ */
29
+ export declare function generateApiKey(options?: GenerateApiKeyOptions): GeneratedApiKey;
30
+ /**
31
+ * Verifies whether a key string is syntactically valid and has an authentic checksum.
32
+ * Does not require a database lookup.
33
+ */
34
+ export declare function verifyApiKeyFormat(key: string, expectedPrefix?: string): boolean;
35
+ /**
36
+ * Validates a raw API key against a stored SHA-256 hash in constant time.
37
+ */
38
+ export declare function verifyApiKey(rawKey: string, storedHash: string, expectedPrefix?: string): boolean;
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getRandomBytes = getRandomBytes;
4
+ exports.bytesToBase62 = bytesToBase62;
5
+ exports.calculateKeyChecksum = calculateKeyChecksum;
6
+ exports.timingSafeEqual = timingSafeEqual;
7
+ exports.hashApiKey = hashApiKey;
8
+ exports.generateApiKey = generateApiKey;
9
+ exports.verifyApiKeyFormat = verifyApiKeyFormat;
10
+ exports.verifyApiKey = verifyApiKey;
11
+ const sha2_js_1 = require("@noble/hashes/sha2.js");
12
+ const utils_js_1 = require("@noble/hashes/utils.js");
13
+ const BASE62_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
14
+ /**
15
+ * Generates cryptographically secure random bytes across Web, Expo/React Native, and Node.js.
16
+ */
17
+ function getRandomBytes(length) {
18
+ const cryptoObj = globalThis.crypto;
19
+ if (cryptoObj?.getRandomValues) {
20
+ return cryptoObj.getRandomValues(new Uint8Array(length));
21
+ }
22
+ try {
23
+ // Dynamic fallback for Node.js environments without globalThis.crypto
24
+ const nodeCrypto = globalThis.process?.versions?.node
25
+ ? globalThis.require('node:crypto')
26
+ : null;
27
+ if (nodeCrypto?.randomBytes) {
28
+ return new Uint8Array(nodeCrypto.randomBytes(length));
29
+ }
30
+ }
31
+ catch {
32
+ // Fallback ignored
33
+ }
34
+ throw new Error('Cryptographically secure random number generator is unavailable.');
35
+ }
36
+ /**
37
+ * Encodes raw bytes into a base62 string.
38
+ */
39
+ function bytesToBase62(bytes) {
40
+ let result = '';
41
+ for (let i = 0; i < bytes.length; i++) {
42
+ result += BASE62_ALPHABET[bytes[i] % 62];
43
+ }
44
+ return result;
45
+ }
46
+ /**
47
+ * Calculates a 4-byte (8-char hex) checksum over the prefix and random entropy.
48
+ */
49
+ function calculateKeyChecksum(content) {
50
+ const hash = (0, sha2_js_1.sha256)((0, utils_js_1.utf8ToBytes)(content));
51
+ return (0, utils_js_1.bytesToHex)(hash.slice(0, 4));
52
+ }
53
+ /**
54
+ * Constant-time string comparison to prevent timing attacks.
55
+ */
56
+ function timingSafeEqual(a, b) {
57
+ if (a.length !== b.length) {
58
+ return false;
59
+ }
60
+ let mismatch = 0;
61
+ for (let i = 0; i < a.length; i++) {
62
+ mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
63
+ }
64
+ return mismatch === 0;
65
+ }
66
+ /**
67
+ * Hashes an API key using SHA-256 for secure storage.
68
+ */
69
+ function hashApiKey(key) {
70
+ return (0, utils_js_1.bytesToHex)((0, sha2_js_1.sha256)((0, utils_js_1.utf8ToBytes)(key.trim())));
71
+ }
72
+ /**
73
+ * Generates an industry-standard, cryptographically secure API key.
74
+ * Format: [prefix]_[entropy]_[checksum]
75
+ *
76
+ * Example:
77
+ * prop_a7F9x2K1m8P0qW4z_c4f910e2
78
+ */
79
+ function generateApiKey(options = {}) {
80
+ const prefix = (options.prefix || 'key').trim().toLowerCase().replace(/[^a-z0-9_-]/g, '');
81
+ const byteLength = options.byteLength ?? 24;
82
+ const withChecksum = options.withChecksum ?? true;
83
+ const entropyBytes = getRandomBytes(byteLength);
84
+ const entropy = bytesToBase62(entropyBytes);
85
+ let key;
86
+ let checksum = '';
87
+ if (withChecksum) {
88
+ checksum = calculateKeyChecksum(`${prefix}_${entropy}`);
89
+ key = `${prefix}_${entropy}_${checksum}`;
90
+ }
91
+ else {
92
+ key = `${prefix}_${entropy}`;
93
+ }
94
+ const keyHash = hashApiKey(key);
95
+ // Masked display representation (e.g. prop_...c4f910e2)
96
+ const suffix = key.slice(-6);
97
+ const display = `${prefix}_...${suffix}`;
98
+ return {
99
+ key,
100
+ keyHash,
101
+ prefix,
102
+ display,
103
+ };
104
+ }
105
+ /**
106
+ * Verifies whether a key string is syntactically valid and has an authentic checksum.
107
+ * Does not require a database lookup.
108
+ */
109
+ function verifyApiKeyFormat(key, expectedPrefix) {
110
+ if (!key || typeof key !== 'string')
111
+ return false;
112
+ const parts = key.trim().split('_');
113
+ if (parts.length < 2)
114
+ return false;
115
+ const prefix = parts[0];
116
+ if (expectedPrefix && prefix !== expectedPrefix)
117
+ return false;
118
+ // Checksummed format: [prefix]_[entropy]_[checksum]
119
+ if (parts.length >= 3) {
120
+ const checksum = parts[parts.length - 1];
121
+ const content = parts.slice(0, parts.length - 1).join('_');
122
+ const expectedChecksum = calculateKeyChecksum(content);
123
+ return timingSafeEqual(checksum, expectedChecksum);
124
+ }
125
+ return true;
126
+ }
127
+ /**
128
+ * Validates a raw API key against a stored SHA-256 hash in constant time.
129
+ */
130
+ function verifyApiKey(rawKey, storedHash, expectedPrefix) {
131
+ if (!verifyApiKeyFormat(rawKey, expectedPrefix)) {
132
+ return false;
133
+ }
134
+ const computedHash = hashApiKey(rawKey);
135
+ return timingSafeEqual(computedHash, storedHash);
136
+ }
@@ -0,0 +1,5 @@
1
+ export * from './types.js';
2
+ export * from './generator.js';
3
+ export * from './signed-token.js';
4
+ export * from './store.js';
5
+ export * from './manager.js';
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./types.js"), exports);
18
+ __exportStar(require("./generator.js"), exports);
19
+ __exportStar(require("./signed-token.js"), exports);
20
+ __exportStar(require("./store.js"), exports);
21
+ __exportStar(require("./manager.js"), exports);
@@ -0,0 +1,59 @@
1
+ import { LocalDBAdapter } from '../adapters/local/index.js';
2
+ import { ApiKeyManagerOptions, ApiKeyRecord, CreateSignedTokenOptions, GenerateApiKeyOptions, RenewSignedTokenOptions, VerifySignedTokenResult, VerifyStoredKeyResult } from './types.js';
3
+ export declare class ApiKeyManager {
4
+ private db;
5
+ private tableName;
6
+ constructor(db: LocalDBAdapter, options?: ApiKeyManagerOptions);
7
+ /**
8
+ * Initializes the API keys database table and indexes.
9
+ */
10
+ initialize(): Promise<void>;
11
+ /**
12
+ * Generates a new cryptographically secure API key and persists its hash to SQLite.
13
+ * Returns the plaintext key (to show to the user once) along with the stored record.
14
+ */
15
+ create(options?: GenerateApiKeyOptions): Promise<{
16
+ key: string;
17
+ record: ApiKeyRecord;
18
+ }>;
19
+ /**
20
+ * Verifies an incoming raw API key against stored hashes in SQLite.
21
+ * Checks format, checksum, expiration, and revocation status.
22
+ */
23
+ verify(rawKey: string, options?: {
24
+ updateLastUsed?: boolean;
25
+ expectedPrefix?: string;
26
+ }): Promise<VerifyStoredKeyResult>;
27
+ /**
28
+ * Revokes an existing API key by ID.
29
+ */
30
+ revoke(id: string): Promise<boolean>;
31
+ /**
32
+ * Lists stored API key records with optional filters.
33
+ */
34
+ list(filter?: {
35
+ revoked?: boolean;
36
+ prefix?: string;
37
+ }): Promise<ApiKeyRecord[]>;
38
+ /**
39
+ * Prunes revoked or expired key records.
40
+ */
41
+ prune(options?: {
42
+ revokedOlderThanMs?: number;
43
+ expiredOlderThanMs?: number;
44
+ }): Promise<number>;
45
+ /**
46
+ * Helper to create a signed, tamper-evident token with expiration.
47
+ */
48
+ createSignedToken<T = Record<string, any>>(options: CreateSignedTokenOptions<T>): string;
49
+ /**
50
+ * Helper to verify a signed token's signature and expiration.
51
+ */
52
+ verifySignedToken<T = Record<string, any>>(token: string, secret: string, options?: {
53
+ clockSkewSeconds?: number;
54
+ }): VerifySignedTokenResult<T>;
55
+ /**
56
+ * Helper to renew an existing signed token by extending its expiration.
57
+ */
58
+ renewSignedToken<T = Record<string, any>>(token: string, secret: string, options?: RenewSignedTokenOptions): string;
59
+ }
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ApiKeyManager = void 0;
4
+ const uuid_js_1 = require("../utils/uuid.js");
5
+ const generator_js_1 = require("./generator.js");
6
+ const signed_token_js_1 = require("./signed-token.js");
7
+ const store_js_1 = require("./store.js");
8
+ class ApiKeyManager {
9
+ db;
10
+ tableName;
11
+ constructor(db, options = {}) {
12
+ this.db = db;
13
+ this.tableName = options.tableName || store_js_1.DEFAULT_KEYS_TABLE;
14
+ }
15
+ /**
16
+ * Initializes the API keys database table and indexes.
17
+ */
18
+ async initialize() {
19
+ await store_js_1.KeyStore.ensureSchema(this.db, this.tableName);
20
+ }
21
+ /**
22
+ * Generates a new cryptographically secure API key and persists its hash to SQLite.
23
+ * Returns the plaintext key (to show to the user once) along with the stored record.
24
+ */
25
+ async create(options = {}) {
26
+ const generated = (0, generator_js_1.generateApiKey)(options);
27
+ const now = Date.now();
28
+ const expiresAt = options.expiresAt !== undefined
29
+ ? options.expiresAt
30
+ : options.expiresInMs !== undefined
31
+ ? now + options.expiresInMs
32
+ : null;
33
+ const record = {
34
+ id: (0, uuid_js_1.generateUUID)(),
35
+ name: options.name || `${generated.prefix} key`,
36
+ prefix: generated.prefix,
37
+ display: generated.display,
38
+ keyHash: generated.keyHash,
39
+ metadata: options.metadata,
40
+ createdAt: now,
41
+ expiresAt,
42
+ lastUsedAt: null,
43
+ revoked: false,
44
+ revokedAt: null,
45
+ };
46
+ await store_js_1.KeyStore.insertRecord(this.db, this.tableName, record);
47
+ return {
48
+ key: generated.key,
49
+ record,
50
+ };
51
+ }
52
+ /**
53
+ * Verifies an incoming raw API key against stored hashes in SQLite.
54
+ * Checks format, checksum, expiration, and revocation status.
55
+ */
56
+ async verify(rawKey, options = {}) {
57
+ if (!rawKey || typeof rawKey !== 'string') {
58
+ return { valid: false, status: 'MALFORMED', error: 'API key is missing or not a string' };
59
+ }
60
+ // 1. Instant syntactic & checksum format verification
61
+ if (!(0, generator_js_1.verifyApiKeyFormat)(rawKey, options.expectedPrefix)) {
62
+ return { valid: false, status: 'MALFORMED', error: 'Invalid API key format or checksum' };
63
+ }
64
+ // 2. Hash and lookup in database
65
+ const computedHash = (0, generator_js_1.hashApiKey)(rawKey);
66
+ const record = await store_js_1.KeyStore.findByHash(this.db, this.tableName, computedHash);
67
+ if (!record) {
68
+ return { valid: false, status: 'INVALID', error: 'API key not found' };
69
+ }
70
+ // 3. Check revocation
71
+ if (record.revoked) {
72
+ return { valid: false, status: 'REVOKED', record, error: 'API key has been revoked' };
73
+ }
74
+ // 4. Check expiration
75
+ if (record.expiresAt && record.expiresAt <= Date.now()) {
76
+ return { valid: false, status: 'EXPIRED', record, error: 'API key has expired' };
77
+ }
78
+ // 5. Update last_used_at timestamp
79
+ if (options.updateLastUsed !== false) {
80
+ const now = Date.now();
81
+ record.lastUsedAt = now;
82
+ store_js_1.KeyStore.updateLastUsed(this.db, this.tableName, record.id, now).catch(() => { });
83
+ }
84
+ return {
85
+ valid: true,
86
+ status: 'VALID',
87
+ record,
88
+ };
89
+ }
90
+ /**
91
+ * Revokes an existing API key by ID.
92
+ */
93
+ async revoke(id) {
94
+ return store_js_1.KeyStore.revoke(this.db, this.tableName, id);
95
+ }
96
+ /**
97
+ * Lists stored API key records with optional filters.
98
+ */
99
+ async list(filter) {
100
+ return store_js_1.KeyStore.list(this.db, this.tableName, filter);
101
+ }
102
+ /**
103
+ * Prunes revoked or expired key records.
104
+ */
105
+ async prune(options) {
106
+ return store_js_1.KeyStore.prune(this.db, this.tableName, options);
107
+ }
108
+ /**
109
+ * Helper to create a signed, tamper-evident token with expiration.
110
+ */
111
+ createSignedToken(options) {
112
+ return (0, signed_token_js_1.createSignedToken)(options);
113
+ }
114
+ /**
115
+ * Helper to verify a signed token's signature and expiration.
116
+ */
117
+ verifySignedToken(token, secret, options) {
118
+ return (0, signed_token_js_1.verifySignedToken)(token, secret, options);
119
+ }
120
+ /**
121
+ * Helper to renew an existing signed token by extending its expiration.
122
+ */
123
+ renewSignedToken(token, secret, options) {
124
+ return (0, signed_token_js_1.renewSignedToken)(token, secret, options);
125
+ }
126
+ }
127
+ exports.ApiKeyManager = ApiKeyManager;
@@ -0,0 +1,18 @@
1
+ import { CreateSignedTokenOptions, RenewSignedTokenOptions, VerifySignedTokenResult } from './types.js';
2
+ export declare function base64UrlEncode(bytes: Uint8Array): string;
3
+ export declare function base64UrlDecode(str: string): Uint8Array;
4
+ /**
5
+ * Creates a signed, tamper-evident token with optional expiration and token ID.
6
+ * Format: [base64urlHeader].[base64urlPayload].[signature]
7
+ */
8
+ export declare function createSignedToken<T = Record<string, any>>(options: CreateSignedTokenOptions<T>): string;
9
+ /**
10
+ * Verifies a signed token's cryptographic HMAC signature and expiration timestamp.
11
+ */
12
+ export declare function verifySignedToken<T = Record<string, any>>(token: string, secret: string, options?: {
13
+ clockSkewSeconds?: number;
14
+ }): VerifySignedTokenResult<T>;
15
+ /**
16
+ * Renews an existing valid signed token by extending its expiration time with a fresh signature.
17
+ */
18
+ export declare function renewSignedToken<T = Record<string, any>>(token: string, secret: string, options?: RenewSignedTokenOptions): string;