local-lite 0.0.19 → 0.0.21
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/auth/index.d.ts +1 -0
- package/dist/auth/index.js +1 -0
- package/dist/auth/session.d.ts +107 -0
- package/dist/auth/session.js +259 -0
- package/dist/core/client.d.ts +18 -0
- package/dist/core/client.js +55 -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 +3 -0
- package/dist/react/index.js +3 -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/react/session.d.ts +26 -0
- package/dist/react/session.js +118 -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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export interface GenerateApiKeyOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Prefix for the API key (e.g. 'prop', 'sk_live', 'pk_test').
|
|
4
|
+
* Default: 'key'
|
|
5
|
+
*/
|
|
6
|
+
prefix?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Number of random entropy bytes (higher is more secure).
|
|
9
|
+
* Default: 24 bytes (192 bits of entropy)
|
|
10
|
+
*/
|
|
11
|
+
byteLength?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Whether to embed a 4-byte checksum at the end of the key for instant validation.
|
|
14
|
+
* Default: true
|
|
15
|
+
*/
|
|
16
|
+
withChecksum?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Human-readable label or description for this key.
|
|
19
|
+
*/
|
|
20
|
+
name?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Arbitrary metadata (e.g. { businessId, scopes, ownerId }).
|
|
23
|
+
*/
|
|
24
|
+
metadata?: Record<string, any>;
|
|
25
|
+
/**
|
|
26
|
+
* Expiration window in milliseconds from creation time.
|
|
27
|
+
*/
|
|
28
|
+
expiresInMs?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Explicit expiration timestamp in milliseconds.
|
|
31
|
+
*/
|
|
32
|
+
expiresAt?: number;
|
|
33
|
+
}
|
|
34
|
+
export interface GeneratedApiKey {
|
|
35
|
+
/**
|
|
36
|
+
* The raw plaintext key string to return to the user (only shown once!).
|
|
37
|
+
* e.g. "prop_a7F9x..._c4b2"
|
|
38
|
+
*/
|
|
39
|
+
key: string;
|
|
40
|
+
/**
|
|
41
|
+
* Cryptographic SHA-256 hash of the key for secure database storage.
|
|
42
|
+
*/
|
|
43
|
+
keyHash: string;
|
|
44
|
+
/**
|
|
45
|
+
* Key prefix.
|
|
46
|
+
*/
|
|
47
|
+
prefix: string;
|
|
48
|
+
/**
|
|
49
|
+
* Masked display string safe to show in UI (e.g. "prop_...c4b2").
|
|
50
|
+
*/
|
|
51
|
+
display: string;
|
|
52
|
+
}
|
|
53
|
+
export interface ApiKeyRecord {
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
prefix: string;
|
|
57
|
+
display: string;
|
|
58
|
+
keyHash: string;
|
|
59
|
+
metadata?: Record<string, any>;
|
|
60
|
+
createdAt: number;
|
|
61
|
+
expiresAt?: number | null;
|
|
62
|
+
lastUsedAt?: number | null;
|
|
63
|
+
revoked: boolean;
|
|
64
|
+
revokedAt?: number | null;
|
|
65
|
+
}
|
|
66
|
+
export type KeyVerificationStatus = 'VALID' | 'INVALID' | 'EXPIRED' | 'REVOKED' | 'MALFORMED';
|
|
67
|
+
export interface VerifyStoredKeyResult {
|
|
68
|
+
valid: boolean;
|
|
69
|
+
status: KeyVerificationStatus;
|
|
70
|
+
record?: ApiKeyRecord;
|
|
71
|
+
error?: string;
|
|
72
|
+
}
|
|
73
|
+
export interface CreateSignedTokenOptions<T = Record<string, any>> {
|
|
74
|
+
payload: T;
|
|
75
|
+
secret: string;
|
|
76
|
+
expiresInMs?: number;
|
|
77
|
+
expiresAt?: number;
|
|
78
|
+
tokenId?: string;
|
|
79
|
+
}
|
|
80
|
+
export interface VerifySignedTokenResult<T = Record<string, any>> {
|
|
81
|
+
valid: boolean;
|
|
82
|
+
expired?: boolean;
|
|
83
|
+
payload?: T;
|
|
84
|
+
expiresAt?: number | null;
|
|
85
|
+
tokenId?: string;
|
|
86
|
+
error?: string;
|
|
87
|
+
}
|
|
88
|
+
export interface RenewSignedTokenOptions {
|
|
89
|
+
extendMs?: number;
|
|
90
|
+
newExpiresAt?: number;
|
|
91
|
+
secret?: string;
|
|
92
|
+
}
|
|
93
|
+
export interface ApiKeyManagerOptions {
|
|
94
|
+
tableName?: string;
|
|
95
|
+
}
|
package/dist/kv/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
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("./store.js"), exports);
|
|
19
|
+
__exportStar(require("./manager.js"), exports);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { DBExecutable } from '../core/schema-manager.js';
|
|
2
|
+
import { EventEmitter } from '../reactive/event-emitter.js';
|
|
3
|
+
import { KVRecord, KVSetOptions, KVSetSecretOptions, KVStoreOptions } from './types.js';
|
|
4
|
+
export declare class KVManager {
|
|
5
|
+
private db;
|
|
6
|
+
private tableName;
|
|
7
|
+
private events?;
|
|
8
|
+
constructor(db: DBExecutable, eventsOrOptions?: EventEmitter | KVStoreOptions, options?: KVStoreOptions);
|
|
9
|
+
initialize(): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Sets a value in the key-value store with optional TTL.
|
|
12
|
+
* Emits an event on the event emitter if configured.
|
|
13
|
+
*/
|
|
14
|
+
set(key: string, value: any, options?: KVSetOptions): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Retrieves the value for a key, returning defaultValue if not found or expired.
|
|
17
|
+
*/
|
|
18
|
+
get<T = any>(key: string, defaultValue?: T): Promise<T | undefined>;
|
|
19
|
+
/**
|
|
20
|
+
* Retrieves the full KVRecord metadata for a key.
|
|
21
|
+
*/
|
|
22
|
+
getRecord(key: string): Promise<KVRecord | null>;
|
|
23
|
+
/**
|
|
24
|
+
* Checks whether a non-expired key exists in the store.
|
|
25
|
+
*/
|
|
26
|
+
has(key: string): Promise<boolean>;
|
|
27
|
+
/**
|
|
28
|
+
* Removes a key from the store.
|
|
29
|
+
* Emits an event on the event emitter if configured.
|
|
30
|
+
*/
|
|
31
|
+
delete(key: string): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Lists all non-expired keys, optionally filtered by a prefix.
|
|
34
|
+
*/
|
|
35
|
+
keys(prefix?: string): Promise<string[]>;
|
|
36
|
+
/**
|
|
37
|
+
* Clears all stored key-value pairs.
|
|
38
|
+
*/
|
|
39
|
+
clear(): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Stores an encrypted secret using AES-256-GCM + PBKDF2 passphrase encryption.
|
|
42
|
+
*/
|
|
43
|
+
setSecret(key: string, secretValue: any, options: KVSetSecretOptions): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Decrypts and retrieves a stored secret.
|
|
46
|
+
*/
|
|
47
|
+
getSecret<T = any>(key: string, passphrase: string, defaultValue?: T): Promise<T | undefined>;
|
|
48
|
+
/**
|
|
49
|
+
* Purges all expired keys immediately.
|
|
50
|
+
*/
|
|
51
|
+
pruneExpired(): Promise<number>;
|
|
52
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KVManager = void 0;
|
|
4
|
+
const encryption_js_1 = require("../backups/encryption.js");
|
|
5
|
+
const signed_token_js_1 = require("../keys/signed-token.js");
|
|
6
|
+
const store_js_1 = require("./store.js");
|
|
7
|
+
class KVManager {
|
|
8
|
+
db;
|
|
9
|
+
tableName;
|
|
10
|
+
events;
|
|
11
|
+
constructor(db, eventsOrOptions, options) {
|
|
12
|
+
this.db = db;
|
|
13
|
+
let opts = {};
|
|
14
|
+
if (eventsOrOptions && typeof eventsOrOptions.emit === 'function') {
|
|
15
|
+
this.events = eventsOrOptions;
|
|
16
|
+
opts = options || {};
|
|
17
|
+
}
|
|
18
|
+
else if (eventsOrOptions) {
|
|
19
|
+
opts = eventsOrOptions;
|
|
20
|
+
}
|
|
21
|
+
this.tableName = opts.tableName ?? store_js_1.DEFAULT_KV_TABLE;
|
|
22
|
+
}
|
|
23
|
+
async initialize() {
|
|
24
|
+
await store_js_1.KVStore.ensureSchema(this.db, this.tableName);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Sets a value in the key-value store with optional TTL.
|
|
28
|
+
* Emits an event on the event emitter if configured.
|
|
29
|
+
*/
|
|
30
|
+
async set(key, value, options = {}) {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
const expiresAt = options.expiresAt !== undefined
|
|
33
|
+
? options.expiresAt
|
|
34
|
+
: options.ttlMs !== undefined
|
|
35
|
+
? now + options.ttlMs
|
|
36
|
+
: null;
|
|
37
|
+
let type;
|
|
38
|
+
let valueStr;
|
|
39
|
+
if (typeof value === 'string') {
|
|
40
|
+
type = 'string';
|
|
41
|
+
valueStr = value;
|
|
42
|
+
}
|
|
43
|
+
else if (typeof value === 'number') {
|
|
44
|
+
type = 'number';
|
|
45
|
+
valueStr = String(value);
|
|
46
|
+
}
|
|
47
|
+
else if (typeof value === 'boolean') {
|
|
48
|
+
type = 'boolean';
|
|
49
|
+
valueStr = value ? '1' : '0';
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
type = 'json';
|
|
53
|
+
valueStr = JSON.stringify(value ?? null);
|
|
54
|
+
}
|
|
55
|
+
await store_js_1.KVStore.set(this.db, this.tableName, key, valueStr, type, now, expiresAt);
|
|
56
|
+
this.events?.emit(this.tableName, { operation: 'SET', key, value });
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Retrieves the value for a key, returning defaultValue if not found or expired.
|
|
60
|
+
*/
|
|
61
|
+
async get(key, defaultValue) {
|
|
62
|
+
const record = await store_js_1.KVStore.get(this.db, this.tableName, key);
|
|
63
|
+
if (!record)
|
|
64
|
+
return defaultValue;
|
|
65
|
+
if (record.expiresAt && record.expiresAt <= Date.now()) {
|
|
66
|
+
// Lazy deletion of expired key
|
|
67
|
+
store_js_1.KVStore.delete(this.db, this.tableName, key).catch(() => { });
|
|
68
|
+
return defaultValue;
|
|
69
|
+
}
|
|
70
|
+
return record.value;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Retrieves the full KVRecord metadata for a key.
|
|
74
|
+
*/
|
|
75
|
+
async getRecord(key) {
|
|
76
|
+
const record = await store_js_1.KVStore.get(this.db, this.tableName, key);
|
|
77
|
+
if (!record)
|
|
78
|
+
return null;
|
|
79
|
+
if (record.expiresAt && record.expiresAt <= Date.now()) {
|
|
80
|
+
store_js_1.KVStore.delete(this.db, this.tableName, key).catch(() => { });
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return record;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Checks whether a non-expired key exists in the store.
|
|
87
|
+
*/
|
|
88
|
+
async has(key) {
|
|
89
|
+
const val = await this.get(key);
|
|
90
|
+
return val !== undefined;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Removes a key from the store.
|
|
94
|
+
* Emits an event on the event emitter if configured.
|
|
95
|
+
*/
|
|
96
|
+
async delete(key) {
|
|
97
|
+
await store_js_1.KVStore.delete(this.db, this.tableName, key);
|
|
98
|
+
this.events?.emit(this.tableName, { operation: 'DELETE', key });
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Lists all non-expired keys, optionally filtered by a prefix.
|
|
102
|
+
*/
|
|
103
|
+
async keys(prefix) {
|
|
104
|
+
return store_js_1.KVStore.listKeys(this.db, this.tableName, prefix);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Clears all stored key-value pairs.
|
|
108
|
+
*/
|
|
109
|
+
async clear() {
|
|
110
|
+
await store_js_1.KVStore.clear(this.db, this.tableName);
|
|
111
|
+
this.events?.emit(this.tableName, { operation: 'CLEAR' });
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Stores an encrypted secret using AES-256-GCM + PBKDF2 passphrase encryption.
|
|
115
|
+
*/
|
|
116
|
+
async setSecret(key, secretValue, options) {
|
|
117
|
+
if (!options.passphrase) {
|
|
118
|
+
throw new Error('Passphrase is required to store encrypted secret.');
|
|
119
|
+
}
|
|
120
|
+
const strValue = typeof secretValue === 'string' ? secretValue : JSON.stringify(secretValue);
|
|
121
|
+
const plainBytes = new TextEncoder().encode(strValue);
|
|
122
|
+
const encryptedBytes = await (0, encryption_js_1.encryptBackupData)(plainBytes, options.passphrase);
|
|
123
|
+
const encryptedBase64 = (0, signed_token_js_1.base64UrlEncode)(encryptedBytes);
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
const expiresAt = options.expiresAt !== undefined
|
|
126
|
+
? options.expiresAt
|
|
127
|
+
: options.ttlMs !== undefined
|
|
128
|
+
? now + options.ttlMs
|
|
129
|
+
: null;
|
|
130
|
+
await store_js_1.KVStore.set(this.db, this.tableName, key, encryptedBase64, 'encrypted', now, expiresAt);
|
|
131
|
+
this.events?.emit(this.tableName, { operation: 'SET_SECRET', key });
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Decrypts and retrieves a stored secret.
|
|
135
|
+
*/
|
|
136
|
+
async getSecret(key, passphrase, defaultValue) {
|
|
137
|
+
const record = await store_js_1.KVStore.get(this.db, this.tableName, key);
|
|
138
|
+
if (!record)
|
|
139
|
+
return defaultValue;
|
|
140
|
+
if (record.expiresAt && record.expiresAt <= Date.now()) {
|
|
141
|
+
store_js_1.KVStore.delete(this.db, this.tableName, key).catch(() => { });
|
|
142
|
+
return defaultValue;
|
|
143
|
+
}
|
|
144
|
+
if (record.type !== 'encrypted') {
|
|
145
|
+
return record.value;
|
|
146
|
+
}
|
|
147
|
+
const encryptedBytes = (0, signed_token_js_1.base64UrlDecode)(record.value);
|
|
148
|
+
const decryptedBytes = await (0, encryption_js_1.decryptBackupData)(encryptedBytes, passphrase);
|
|
149
|
+
const text = new TextDecoder().decode(decryptedBytes);
|
|
150
|
+
try {
|
|
151
|
+
return JSON.parse(text);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return text;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Purges all expired keys immediately.
|
|
159
|
+
*/
|
|
160
|
+
async pruneExpired() {
|
|
161
|
+
return store_js_1.KVStore.pruneExpired(this.db, this.tableName);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
exports.KVManager = KVManager;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DBExecutable } from '../core/schema-manager.js';
|
|
2
|
+
import { KVRecord } from './types.js';
|
|
3
|
+
export declare const DEFAULT_KV_TABLE = "_local_kv";
|
|
4
|
+
export declare function mapKVRow(r: any): KVRecord;
|
|
5
|
+
export declare class KVStore {
|
|
6
|
+
static ensureSchema(db: DBExecutable, tableName?: string): Promise<void>;
|
|
7
|
+
static set(db: DBExecutable, tableName: string | undefined, key: string, valueStr: string, type: string, now: number, expiresAt: number | null): Promise<void>;
|
|
8
|
+
static get(db: DBExecutable, tableName: string | undefined, key: string): Promise<KVRecord | null>;
|
|
9
|
+
static delete(db: DBExecutable, tableName: string | undefined, key: string): Promise<boolean>;
|
|
10
|
+
static listKeys(db: DBExecutable, tableName?: string, prefix?: string): Promise<string[]>;
|
|
11
|
+
static clear(db: DBExecutable, tableName?: string): Promise<void>;
|
|
12
|
+
static pruneExpired(db: DBExecutable, tableName?: string): Promise<number>;
|
|
13
|
+
}
|
package/dist/kv/store.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KVStore = exports.DEFAULT_KV_TABLE = void 0;
|
|
4
|
+
exports.mapKVRow = mapKVRow;
|
|
5
|
+
exports.DEFAULT_KV_TABLE = '_local_kv';
|
|
6
|
+
function mapKVRow(r) {
|
|
7
|
+
let val = r.value;
|
|
8
|
+
const type = r.type;
|
|
9
|
+
if (type === 'number') {
|
|
10
|
+
val = Number(val);
|
|
11
|
+
}
|
|
12
|
+
else if (type === 'boolean') {
|
|
13
|
+
val = val === 'true' || val === '1' || val === 1;
|
|
14
|
+
}
|
|
15
|
+
else if (type === 'json') {
|
|
16
|
+
try {
|
|
17
|
+
val = JSON.parse(val);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// fallback
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
key: r.key,
|
|
25
|
+
value: val,
|
|
26
|
+
type: r.type,
|
|
27
|
+
createdAt: Number(r.created_at || 0),
|
|
28
|
+
updatedAt: Number(r.updated_at || 0),
|
|
29
|
+
expiresAt: r.expires_at != null ? Number(r.expires_at) : null,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
class KVStore {
|
|
33
|
+
static async ensureSchema(db, tableName = exports.DEFAULT_KV_TABLE) {
|
|
34
|
+
await db.execute(`CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
35
|
+
key TEXT PRIMARY KEY,
|
|
36
|
+
value TEXT NOT NULL,
|
|
37
|
+
type TEXT NOT NULL,
|
|
38
|
+
created_at INTEGER NOT NULL,
|
|
39
|
+
updated_at INTEGER NOT NULL,
|
|
40
|
+
expires_at INTEGER
|
|
41
|
+
);`);
|
|
42
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS idx_${tableName}_expires ON ${tableName} (expires_at);`);
|
|
43
|
+
}
|
|
44
|
+
static async set(db, tableName = exports.DEFAULT_KV_TABLE, key, valueStr, type, now, expiresAt) {
|
|
45
|
+
await db.execute(`INSERT INTO ${tableName} (key, value, type, created_at, updated_at, expires_at)
|
|
46
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
47
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
48
|
+
value = excluded.value,
|
|
49
|
+
type = excluded.type,
|
|
50
|
+
updated_at = excluded.updated_at,
|
|
51
|
+
expires_at = excluded.expires_at;`, [key, valueStr, type, now, now, expiresAt]);
|
|
52
|
+
}
|
|
53
|
+
static async get(db, tableName = exports.DEFAULT_KV_TABLE, key) {
|
|
54
|
+
const rows = await db.execute(`SELECT * FROM ${tableName} WHERE key = ? LIMIT 1;`, [key]);
|
|
55
|
+
if (!rows || rows.length === 0)
|
|
56
|
+
return null;
|
|
57
|
+
return mapKVRow(rows[0]);
|
|
58
|
+
}
|
|
59
|
+
static async delete(db, tableName = exports.DEFAULT_KV_TABLE, key) {
|
|
60
|
+
await db.execute(`DELETE FROM ${tableName} WHERE key = ?;`, [key]);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
static async listKeys(db, tableName = exports.DEFAULT_KV_TABLE, prefix) {
|
|
64
|
+
let rows;
|
|
65
|
+
if (prefix) {
|
|
66
|
+
rows = await db.execute(`SELECT key, expires_at FROM ${tableName} WHERE key LIKE ? ORDER BY key ASC;`, [`${prefix}%`]);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
rows = await db.execute(`SELECT key, expires_at FROM ${tableName} ORDER BY key ASC;`);
|
|
70
|
+
}
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
return (rows || [])
|
|
73
|
+
.filter((r) => r.expires_at == null || Number(r.expires_at) > now)
|
|
74
|
+
.map((r) => r.key);
|
|
75
|
+
}
|
|
76
|
+
static async clear(db, tableName = exports.DEFAULT_KV_TABLE) {
|
|
77
|
+
await db.execute(`DELETE FROM ${tableName};`);
|
|
78
|
+
}
|
|
79
|
+
static async pruneExpired(db, tableName = exports.DEFAULT_KV_TABLE) {
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
await db.execute(`DELETE FROM ${tableName} WHERE expires_at IS NOT NULL AND expires_at <= ?;`, [now]);
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
exports.KVStore = KVStore;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface KVSetOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Time-to-live in milliseconds from current time.
|
|
4
|
+
*/
|
|
5
|
+
ttlMs?: number;
|
|
6
|
+
/**
|
|
7
|
+
* Explicit Unix timestamp in milliseconds when this key expires.
|
|
8
|
+
*/
|
|
9
|
+
expiresAt?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface KVSetSecretOptions extends KVSetOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Passphrase used to encrypt the value using AES-256-GCM.
|
|
14
|
+
*/
|
|
15
|
+
passphrase: string;
|
|
16
|
+
}
|
|
17
|
+
export interface KVRecord<T = any> {
|
|
18
|
+
key: string;
|
|
19
|
+
value: T;
|
|
20
|
+
type: 'string' | 'number' | 'boolean' | 'json' | 'encrypted';
|
|
21
|
+
createdAt: number;
|
|
22
|
+
updatedAt: number;
|
|
23
|
+
expiresAt?: number | null;
|
|
24
|
+
}
|
|
25
|
+
export interface KVStoreOptions {
|
|
26
|
+
tableName?: string;
|
|
27
|
+
}
|
package/dist/kv/types.js
ADDED
package/dist/react/index.d.ts
CHANGED
|
@@ -131,3 +131,6 @@ export { SyncInspector } from './SyncInspector.js';
|
|
|
131
131
|
export type { SyncInspectorProps } from './SyncInspector.js';
|
|
132
132
|
export * from './auth.js';
|
|
133
133
|
export * from './backups.js';
|
|
134
|
+
export * from './kv.js';
|
|
135
|
+
export * from './search.js';
|
|
136
|
+
export * from './session.js';
|
package/dist/react/index.js
CHANGED
|
@@ -646,3 +646,6 @@ var SyncInspector_js_1 = require("./SyncInspector.js");
|
|
|
646
646
|
Object.defineProperty(exports, "SyncInspector", { enumerable: true, get: function () { return SyncInspector_js_1.SyncInspector; } });
|
|
647
647
|
__exportStar(require("./auth.js"), exports);
|
|
648
648
|
__exportStar(require("./backups.js"), exports);
|
|
649
|
+
__exportStar(require("./kv.js"), exports);
|
|
650
|
+
__exportStar(require("./search.js"), exports);
|
|
651
|
+
__exportStar(require("./session.js"), exports);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { LocalFirstClient } from '../core/client.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reactive React hook for client.kv key-value preferences.
|
|
4
|
+
* Automatically re-renders whenever the key is updated or deleted.
|
|
5
|
+
*
|
|
6
|
+
* Example:
|
|
7
|
+
* const [theme, setTheme, loading] = useKV(client, 'theme', 'light');
|
|
8
|
+
*/
|
|
9
|
+
export declare function useKV<T = any>(client: LocalFirstClient | null | undefined, key: string, defaultValue?: T): [T | undefined, (value: T) => Promise<void>, boolean];
|
package/dist/react/kv.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useKV = useKV;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
/**
|
|
6
|
+
* Reactive React hook for client.kv key-value preferences.
|
|
7
|
+
* Automatically re-renders whenever the key is updated or deleted.
|
|
8
|
+
*
|
|
9
|
+
* Example:
|
|
10
|
+
* const [theme, setTheme, loading] = useKV(client, 'theme', 'light');
|
|
11
|
+
*/
|
|
12
|
+
function useKV(client, key, defaultValue) {
|
|
13
|
+
const [value, setValueState] = (0, react_1.useState)(defaultValue);
|
|
14
|
+
const [loading, setLoading] = (0, react_1.useState)(true);
|
|
15
|
+
(0, react_1.useEffect)(() => {
|
|
16
|
+
if (!client) {
|
|
17
|
+
setValueState(defaultValue);
|
|
18
|
+
setLoading(false);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
let isMounted = true;
|
|
22
|
+
const load = async () => {
|
|
23
|
+
try {
|
|
24
|
+
const val = await client.kv.get(key, defaultValue);
|
|
25
|
+
if (isMounted) {
|
|
26
|
+
setValueState(val);
|
|
27
|
+
setLoading(false);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
if (isMounted)
|
|
32
|
+
setLoading(false);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
load();
|
|
36
|
+
const unsubscribe = client.events.on('_local_kv', (e) => {
|
|
37
|
+
if (!e || e.key === key || !e.key) {
|
|
38
|
+
load();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
return () => {
|
|
42
|
+
isMounted = false;
|
|
43
|
+
unsubscribe();
|
|
44
|
+
};
|
|
45
|
+
}, [client, key, defaultValue]);
|
|
46
|
+
const setKVValue = (0, react_1.useCallback)(async (newValue) => {
|
|
47
|
+
if (!client)
|
|
48
|
+
return;
|
|
49
|
+
await client.kv.set(key, newValue);
|
|
50
|
+
setValueState(newValue);
|
|
51
|
+
}, [client, key]);
|
|
52
|
+
return [value, setKVValue, loading];
|
|
53
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { LocalFirstClient } from '../core/client.js';
|
|
2
|
+
import { SearchQueryOptions, SearchResult } from '../search/types.js';
|
|
3
|
+
export interface UseSearchOptions extends SearchQueryOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Debounce delay in milliseconds (default: 150ms).
|
|
6
|
+
*/
|
|
7
|
+
debounceMs?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Reactive React hook for instant full-text search with automatic debouncing.
|
|
11
|
+
*
|
|
12
|
+
* Example:
|
|
13
|
+
* const { results, loading, error } = useSearch(client, 'properties', searchQuery);
|
|
14
|
+
*/
|
|
15
|
+
export declare function useSearch<T = any>(client: LocalFirstClient | null | undefined, table: string, queryText: string, options?: UseSearchOptions): {
|
|
16
|
+
results: SearchResult<T>[];
|
|
17
|
+
loading: boolean;
|
|
18
|
+
error: Error | null;
|
|
19
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useSearch = useSearch;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
/**
|
|
6
|
+
* Reactive React hook for instant full-text search with automatic debouncing.
|
|
7
|
+
*
|
|
8
|
+
* Example:
|
|
9
|
+
* const { results, loading, error } = useSearch(client, 'properties', searchQuery);
|
|
10
|
+
*/
|
|
11
|
+
function useSearch(client, table, queryText, options) {
|
|
12
|
+
const [results, setResults] = (0, react_1.useState)([]);
|
|
13
|
+
const [loading, setLoading] = (0, react_1.useState)(false);
|
|
14
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
15
|
+
const debounceMs = options?.debounceMs ?? 150;
|
|
16
|
+
(0, react_1.useEffect)(() => {
|
|
17
|
+
if (!client || !table || !queryText.trim()) {
|
|
18
|
+
setResults([]);
|
|
19
|
+
setLoading(false);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
let isMounted = true;
|
|
23
|
+
setLoading(true);
|
|
24
|
+
const timer = setTimeout(async () => {
|
|
25
|
+
try {
|
|
26
|
+
const res = await client.search.query(table, queryText, options);
|
|
27
|
+
if (isMounted) {
|
|
28
|
+
setResults(res);
|
|
29
|
+
setError(null);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
if (isMounted) {
|
|
34
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
if (isMounted) {
|
|
39
|
+
setLoading(false);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}, debounceMs);
|
|
43
|
+
return () => {
|
|
44
|
+
isMounted = false;
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
};
|
|
47
|
+
}, [client, table, queryText, debounceMs]);
|
|
48
|
+
return { results, loading, error };
|
|
49
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { LocalFirstClient } from '../core/client.js';
|
|
2
|
+
import { SessionRecord, CreateSessionOptions } from '../auth/session.js';
|
|
3
|
+
export interface UseSessionResult<TUser = any> {
|
|
4
|
+
session: SessionRecord<TUser> | null;
|
|
5
|
+
user: TUser | null;
|
|
6
|
+
isAuthenticated: boolean;
|
|
7
|
+
isOffline: boolean;
|
|
8
|
+
loading: boolean;
|
|
9
|
+
createSession: (options: CreateSessionOptions<TUser>) => Promise<SessionRecord<TUser>>;
|
|
10
|
+
updateUser: (partialUser: Partial<TUser>) => Promise<SessionRecord<TUser> | null>;
|
|
11
|
+
updateTokens: (tokens: {
|
|
12
|
+
token?: string | null;
|
|
13
|
+
refreshToken?: string | null;
|
|
14
|
+
expiresAt?: number | null;
|
|
15
|
+
}) => Promise<void>;
|
|
16
|
+
signOut: () => Promise<void>;
|
|
17
|
+
refetchSession: () => Promise<SessionRecord<TUser> | null>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Reactive React hook for persistent, offline-first user session management.
|
|
21
|
+
* The session remains authenticated even when offline, ensuring uninterrupted UX.
|
|
22
|
+
*
|
|
23
|
+
* Example:
|
|
24
|
+
* const { user, isAuthenticated, isOffline, signOut } = useSession(client);
|
|
25
|
+
*/
|
|
26
|
+
export declare function useSession<TUser = any>(client: LocalFirstClient | null | undefined): UseSessionResult<TUser>;
|