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.
- package/README.md +5 -1
- package/dist/actions/store.js +1 -1
- package/dist/core/client.d.ts +15 -0
- package/dist/core/client.js +46 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/keys/generator.d.ts +38 -0
- package/dist/keys/generator.js +136 -0
- package/dist/keys/index.d.ts +5 -0
- package/dist/keys/index.js +21 -0
- package/dist/keys/manager.d.ts +59 -0
- package/dist/keys/manager.js +127 -0
- package/dist/keys/signed-token.d.ts +18 -0
- package/dist/keys/signed-token.js +171 -0
- package/dist/keys/store.d.ts +23 -0
- package/dist/keys/store.js +109 -0
- package/dist/keys/types.d.ts +95 -0
- package/dist/keys/types.js +2 -0
- package/dist/kv/index.d.ts +3 -0
- package/dist/kv/index.js +19 -0
- package/dist/kv/manager.d.ts +52 -0
- package/dist/kv/manager.js +164 -0
- package/dist/kv/store.d.ts +13 -0
- package/dist/kv/store.js +85 -0
- package/dist/kv/types.d.ts +27 -0
- package/dist/kv/types.js +2 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.js +2 -0
- package/dist/react/kv.d.ts +9 -0
- package/dist/react/kv.js +53 -0
- package/dist/react/search.d.ts +19 -0
- package/dist/react/search.js +49 -0
- package/dist/reactive/event-emitter.d.ts +1 -0
- package/dist/reactive/event-emitter.js +9 -6
- package/dist/scheduler/cron.d.ts +6 -0
- package/dist/scheduler/cron.js +62 -0
- package/dist/scheduler/index.d.ts +4 -0
- package/dist/scheduler/index.js +20 -0
- package/dist/scheduler/manager.d.ts +59 -0
- package/dist/scheduler/manager.js +186 -0
- package/dist/scheduler/store.d.ts +20 -0
- package/dist/scheduler/store.js +134 -0
- package/dist/scheduler/types.d.ts +44 -0
- package/dist/scheduler/types.js +2 -0
- package/dist/search/index.d.ts +3 -0
- package/dist/search/index.js +19 -0
- package/dist/search/manager.d.ts +42 -0
- package/dist/search/manager.js +123 -0
- package/dist/search/store.d.ts +33 -0
- package/dist/search/store.js +137 -0
- package/dist/search/types.d.ts +49 -0
- package/dist/search/types.js +2 -0
- package/dist/storage/driver.d.ts +25 -0
- package/dist/storage/driver.js +92 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +20 -0
- package/dist/storage/manager.d.ts +48 -0
- package/dist/storage/manager.js +149 -0
- package/dist/storage/store.d.ts +14 -0
- package/dist/storage/store.js +116 -0
- package/dist/storage/types.d.ts +44 -0
- package/dist/storage/types.js +2 -0
- 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
|
---
|
package/dist/actions/store.js
CHANGED
|
@@ -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
|
-
|
|
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';
|
package/dist/core/client.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/core/client.js
CHANGED
|
@@ -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, () =>
|
|
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
|
|
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,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;
|