local-lite 0.0.18 → 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 +6 -1
- package/dist/index.js +6 -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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.base64UrlEncode = base64UrlEncode;
|
|
4
|
+
exports.base64UrlDecode = base64UrlDecode;
|
|
5
|
+
exports.createSignedToken = createSignedToken;
|
|
6
|
+
exports.verifySignedToken = verifySignedToken;
|
|
7
|
+
exports.renewSignedToken = renewSignedToken;
|
|
8
|
+
const hmac_js_1 = require("@noble/hashes/hmac.js");
|
|
9
|
+
const sha2_js_1 = require("@noble/hashes/sha2.js");
|
|
10
|
+
const utils_js_1 = require("@noble/hashes/utils.js");
|
|
11
|
+
const uuid_js_1 = require("../utils/uuid.js");
|
|
12
|
+
const generator_js_1 = require("./generator.js");
|
|
13
|
+
const B64URL_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
14
|
+
const B64URL_LOOKUP = new Uint8Array(256);
|
|
15
|
+
for (let i = 0; i < B64URL_ALPHABET.length; i++) {
|
|
16
|
+
B64URL_LOOKUP[B64URL_ALPHABET.charCodeAt(i)] = i;
|
|
17
|
+
}
|
|
18
|
+
function base64UrlEncode(bytes) {
|
|
19
|
+
let result = '';
|
|
20
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
21
|
+
const first = bytes[i];
|
|
22
|
+
const second = bytes[i + 1];
|
|
23
|
+
const third = bytes[i + 2];
|
|
24
|
+
result += B64URL_ALPHABET[first >> 2];
|
|
25
|
+
result += B64URL_ALPHABET[((first & 3) << 4) | (second === undefined ? 0 : second >> 4)];
|
|
26
|
+
if (second !== undefined) {
|
|
27
|
+
result += B64URL_ALPHABET[((second & 15) << 2) | (third === undefined ? 0 : third >> 6)];
|
|
28
|
+
}
|
|
29
|
+
if (third !== undefined) {
|
|
30
|
+
result += B64URL_ALPHABET[third & 63];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
function base64UrlDecode(str) {
|
|
36
|
+
// Normalize base64url padding
|
|
37
|
+
let clean = str.replace(/[^A-Za-z0-9\-_]/g, '');
|
|
38
|
+
const remainder = clean.length % 4;
|
|
39
|
+
if (remainder === 2)
|
|
40
|
+
clean += '==';
|
|
41
|
+
else if (remainder === 3)
|
|
42
|
+
clean += '=';
|
|
43
|
+
const len = clean.length;
|
|
44
|
+
let byteLen = (len * 3) / 4;
|
|
45
|
+
if (clean.endsWith('=='))
|
|
46
|
+
byteLen -= 2;
|
|
47
|
+
else if (clean.endsWith('='))
|
|
48
|
+
byteLen -= 1;
|
|
49
|
+
const bytes = new Uint8Array(byteLen);
|
|
50
|
+
let byteIndex = 0;
|
|
51
|
+
for (let i = 0; i < len; i += 4) {
|
|
52
|
+
const c0 = B64URL_LOOKUP[clean.charCodeAt(i)];
|
|
53
|
+
const c1 = B64URL_LOOKUP[clean.charCodeAt(i + 1)];
|
|
54
|
+
const c2 = clean[i + 2] === '=' ? 0 : B64URL_LOOKUP[clean.charCodeAt(i + 2)];
|
|
55
|
+
const c3 = clean[i + 3] === '=' ? 0 : B64URL_LOOKUP[clean.charCodeAt(i + 3)];
|
|
56
|
+
bytes[byteIndex++] = (c0 << 2) | (c1 >> 4);
|
|
57
|
+
if (clean[i + 2] !== '=' && byteIndex < byteLen) {
|
|
58
|
+
bytes[byteIndex++] = ((c1 & 15) << 4) | (c2 >> 2);
|
|
59
|
+
}
|
|
60
|
+
if (clean[i + 3] !== '=' && byteIndex < byteLen) {
|
|
61
|
+
bytes[byteIndex++] = ((c2 & 3) << 6) | c3;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return bytes;
|
|
65
|
+
}
|
|
66
|
+
function computeHmacSignature(content, secret) {
|
|
67
|
+
const sigBytes = (0, hmac_js_1.hmac)(sha2_js_1.sha256, (0, utils_js_1.utf8ToBytes)(secret), (0, utils_js_1.utf8ToBytes)(content));
|
|
68
|
+
return base64UrlEncode(sigBytes);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Creates a signed, tamper-evident token with optional expiration and token ID.
|
|
72
|
+
* Format: [base64urlHeader].[base64urlPayload].[signature]
|
|
73
|
+
*/
|
|
74
|
+
function createSignedToken(options) {
|
|
75
|
+
if (!options.secret) {
|
|
76
|
+
throw new Error('Signing secret is required to create a signed token.');
|
|
77
|
+
}
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
const expiresAt = options.expiresAt !== undefined
|
|
80
|
+
? options.expiresAt
|
|
81
|
+
: options.expiresInMs !== undefined
|
|
82
|
+
? now + options.expiresInMs
|
|
83
|
+
: null;
|
|
84
|
+
const header = {
|
|
85
|
+
alg: 'HS256',
|
|
86
|
+
typ: 'LLT',
|
|
87
|
+
};
|
|
88
|
+
const payload = {
|
|
89
|
+
data: options.payload,
|
|
90
|
+
iat: now,
|
|
91
|
+
exp: expiresAt,
|
|
92
|
+
jti: options.tokenId || (0, uuid_js_1.generateUUID)(),
|
|
93
|
+
};
|
|
94
|
+
const headerB64 = base64UrlEncode((0, utils_js_1.utf8ToBytes)(JSON.stringify(header)));
|
|
95
|
+
const payloadB64 = base64UrlEncode((0, utils_js_1.utf8ToBytes)(JSON.stringify(payload)));
|
|
96
|
+
const content = `${headerB64}.${payloadB64}`;
|
|
97
|
+
const signature = computeHmacSignature(content, options.secret);
|
|
98
|
+
return `${content}.${signature}`;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Verifies a signed token's cryptographic HMAC signature and expiration timestamp.
|
|
102
|
+
*/
|
|
103
|
+
function verifySignedToken(token, secret, options = {}) {
|
|
104
|
+
if (!token || typeof token !== 'string') {
|
|
105
|
+
return { valid: false, error: 'Token is missing or empty' };
|
|
106
|
+
}
|
|
107
|
+
if (!secret) {
|
|
108
|
+
return { valid: false, error: 'Verification secret is required' };
|
|
109
|
+
}
|
|
110
|
+
const parts = token.trim().split('.');
|
|
111
|
+
if (parts.length !== 3) {
|
|
112
|
+
return { valid: false, error: 'Malformed token structure: expected 3 parts' };
|
|
113
|
+
}
|
|
114
|
+
const [headerB64, payloadB64, signature] = parts;
|
|
115
|
+
const content = `${headerB64}.${payloadB64}`;
|
|
116
|
+
// 1. Verify HMAC Signature
|
|
117
|
+
const expectedSignature = computeHmacSignature(content, secret);
|
|
118
|
+
if (!(0, generator_js_1.timingSafeEqual)(signature, expectedSignature)) {
|
|
119
|
+
return { valid: false, error: 'Invalid token signature' };
|
|
120
|
+
}
|
|
121
|
+
// 2. Decode Payload
|
|
122
|
+
let parsedPayload;
|
|
123
|
+
try {
|
|
124
|
+
const jsonStr = new TextDecoder().decode(base64UrlDecode(payloadB64));
|
|
125
|
+
parsedPayload = JSON.parse(jsonStr);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
return { valid: false, error: `Failed to decode token payload: ${err?.message || err}` };
|
|
129
|
+
}
|
|
130
|
+
// 3. Verify Expiration
|
|
131
|
+
const clockSkewMs = (options.clockSkewSeconds ?? 60) * 1000;
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
const exp = parsedPayload.exp;
|
|
134
|
+
if (exp != null && typeof exp === 'number') {
|
|
135
|
+
if (now > exp + clockSkewMs) {
|
|
136
|
+
return {
|
|
137
|
+
valid: false,
|
|
138
|
+
expired: true,
|
|
139
|
+
expiresAt: exp,
|
|
140
|
+
tokenId: parsedPayload.jti,
|
|
141
|
+
payload: parsedPayload.data,
|
|
142
|
+
error: `Token expired at ${new Date(exp).toISOString()}`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
valid: true,
|
|
148
|
+
expired: false,
|
|
149
|
+
payload: parsedPayload.data,
|
|
150
|
+
expiresAt: exp ?? null,
|
|
151
|
+
tokenId: parsedPayload.jti,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Renews an existing valid signed token by extending its expiration time with a fresh signature.
|
|
156
|
+
*/
|
|
157
|
+
function renewSignedToken(token, secret, options = {}) {
|
|
158
|
+
const verification = verifySignedToken(token, secret);
|
|
159
|
+
if (!verification.valid && !verification.expired) {
|
|
160
|
+
throw new Error(`Cannot renew token: ${verification.error}`);
|
|
161
|
+
}
|
|
162
|
+
const extendMs = options.extendMs ?? 7 * 24 * 60 * 60 * 1000; // default 7 days
|
|
163
|
+
const newSecret = options.secret || secret;
|
|
164
|
+
return createSignedToken({
|
|
165
|
+
payload: verification.payload,
|
|
166
|
+
secret: newSecret,
|
|
167
|
+
expiresAt: options.newExpiresAt,
|
|
168
|
+
expiresInMs: options.newExpiresAt ? undefined : extendMs,
|
|
169
|
+
tokenId: verification.tokenId,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { DBExecutable } from '../core/schema-manager.js';
|
|
2
|
+
import { ApiKeyRecord } from './types.js';
|
|
3
|
+
export declare const DEFAULT_KEYS_TABLE = "_local_api_keys";
|
|
4
|
+
export declare function mapKeyRow(r: any): ApiKeyRecord;
|
|
5
|
+
export declare class KeyStore {
|
|
6
|
+
/**
|
|
7
|
+
* Initializes the API keys schema and indexes if they do not exist.
|
|
8
|
+
*/
|
|
9
|
+
static ensureSchema(db: DBExecutable, tableName?: string): Promise<void>;
|
|
10
|
+
static insertRecord(db: DBExecutable, tableName: string | undefined, record: ApiKeyRecord): Promise<void>;
|
|
11
|
+
static findByHash(db: DBExecutable, tableName: string | undefined, hash: string): Promise<ApiKeyRecord | null>;
|
|
12
|
+
static findById(db: DBExecutable, tableName: string | undefined, id: string): Promise<ApiKeyRecord | null>;
|
|
13
|
+
static updateLastUsed(db: DBExecutable, tableName: string | undefined, id: string, timestamp: number): Promise<void>;
|
|
14
|
+
static revoke(db: DBExecutable, tableName: string | undefined, id: string): Promise<boolean>;
|
|
15
|
+
static list(db: DBExecutable, tableName?: string, filter?: {
|
|
16
|
+
revoked?: boolean;
|
|
17
|
+
prefix?: string;
|
|
18
|
+
}): Promise<ApiKeyRecord[]>;
|
|
19
|
+
static prune(db: DBExecutable, tableName?: string, options?: {
|
|
20
|
+
revokedOlderThanMs?: number;
|
|
21
|
+
expiredOlderThanMs?: number;
|
|
22
|
+
}): Promise<number>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KeyStore = exports.DEFAULT_KEYS_TABLE = void 0;
|
|
4
|
+
exports.mapKeyRow = mapKeyRow;
|
|
5
|
+
exports.DEFAULT_KEYS_TABLE = '_local_api_keys';
|
|
6
|
+
function mapKeyRow(r) {
|
|
7
|
+
return {
|
|
8
|
+
id: r.id,
|
|
9
|
+
name: r.name,
|
|
10
|
+
prefix: r.prefix,
|
|
11
|
+
display: r.display,
|
|
12
|
+
keyHash: r.key_hash,
|
|
13
|
+
metadata: r.metadata ? (typeof r.metadata === 'string' ? JSON.parse(r.metadata) : r.metadata) : undefined,
|
|
14
|
+
createdAt: Number(r.created_at || 0),
|
|
15
|
+
expiresAt: r.expires_at != null ? Number(r.expires_at) : null,
|
|
16
|
+
lastUsedAt: r.last_used_at != null ? Number(r.last_used_at) : null,
|
|
17
|
+
revoked: Boolean(Number(r.revoked || 0)),
|
|
18
|
+
revokedAt: r.revoked_at != null ? Number(r.revoked_at) : null,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
class KeyStore {
|
|
22
|
+
/**
|
|
23
|
+
* Initializes the API keys schema and indexes if they do not exist.
|
|
24
|
+
*/
|
|
25
|
+
static async ensureSchema(db, tableName = exports.DEFAULT_KEYS_TABLE) {
|
|
26
|
+
await db.execute(`CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
27
|
+
id TEXT PRIMARY KEY,
|
|
28
|
+
name TEXT NOT NULL,
|
|
29
|
+
prefix TEXT NOT NULL,
|
|
30
|
+
display TEXT NOT NULL,
|
|
31
|
+
key_hash TEXT NOT NULL UNIQUE,
|
|
32
|
+
metadata TEXT,
|
|
33
|
+
created_at INTEGER NOT NULL,
|
|
34
|
+
expires_at INTEGER,
|
|
35
|
+
last_used_at INTEGER,
|
|
36
|
+
revoked INTEGER NOT NULL DEFAULT 0,
|
|
37
|
+
revoked_at INTEGER
|
|
38
|
+
);`);
|
|
39
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS idx_${tableName}_hash ON ${tableName} (key_hash);`);
|
|
40
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS idx_${tableName}_revoked ON ${tableName} (revoked, expires_at);`);
|
|
41
|
+
}
|
|
42
|
+
static async insertRecord(db, tableName = exports.DEFAULT_KEYS_TABLE, record) {
|
|
43
|
+
await db.execute(`INSERT INTO ${tableName} (
|
|
44
|
+
id, name, prefix, display, key_hash, metadata, created_at, expires_at, last_used_at, revoked, revoked_at
|
|
45
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`, [
|
|
46
|
+
record.id,
|
|
47
|
+
record.name,
|
|
48
|
+
record.prefix,
|
|
49
|
+
record.display,
|
|
50
|
+
record.keyHash,
|
|
51
|
+
record.metadata ? JSON.stringify(record.metadata) : null,
|
|
52
|
+
record.createdAt,
|
|
53
|
+
record.expiresAt ?? null,
|
|
54
|
+
record.lastUsedAt ?? null,
|
|
55
|
+
record.revoked ? 1 : 0,
|
|
56
|
+
record.revokedAt ?? null,
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
static async findByHash(db, tableName = exports.DEFAULT_KEYS_TABLE, hash) {
|
|
60
|
+
const rows = await db.execute(`SELECT * FROM ${tableName} WHERE key_hash = ? LIMIT 1;`, [hash]);
|
|
61
|
+
if (!rows || rows.length === 0)
|
|
62
|
+
return null;
|
|
63
|
+
return mapKeyRow(rows[0]);
|
|
64
|
+
}
|
|
65
|
+
static async findById(db, tableName = exports.DEFAULT_KEYS_TABLE, id) {
|
|
66
|
+
const rows = await db.execute(`SELECT * FROM ${tableName} WHERE id = ? LIMIT 1;`, [id]);
|
|
67
|
+
if (!rows || rows.length === 0)
|
|
68
|
+
return null;
|
|
69
|
+
return mapKeyRow(rows[0]);
|
|
70
|
+
}
|
|
71
|
+
static async updateLastUsed(db, tableName = exports.DEFAULT_KEYS_TABLE, id, timestamp) {
|
|
72
|
+
await db.execute(`UPDATE ${tableName} SET last_used_at = ? WHERE id = ?;`, [timestamp, id]);
|
|
73
|
+
}
|
|
74
|
+
static async revoke(db, tableName = exports.DEFAULT_KEYS_TABLE, id) {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
await db.execute(`UPDATE ${tableName} SET revoked = 1, revoked_at = ? WHERE id = ? AND revoked = 0;`, [now, id]);
|
|
77
|
+
const updated = await this.findById(db, tableName, id);
|
|
78
|
+
return updated?.revoked === true;
|
|
79
|
+
}
|
|
80
|
+
static async list(db, tableName = exports.DEFAULT_KEYS_TABLE, filter = {}) {
|
|
81
|
+
const conditions = [];
|
|
82
|
+
const params = [];
|
|
83
|
+
if (filter.revoked !== undefined) {
|
|
84
|
+
conditions.push('revoked = ?');
|
|
85
|
+
params.push(filter.revoked ? 1 : 0);
|
|
86
|
+
}
|
|
87
|
+
if (filter.prefix !== undefined) {
|
|
88
|
+
conditions.push('prefix = ?');
|
|
89
|
+
params.push(filter.prefix);
|
|
90
|
+
}
|
|
91
|
+
const whereClause = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '';
|
|
92
|
+
const rows = await db.execute(`SELECT * FROM ${tableName}${whereClause} ORDER BY created_at DESC;`, params);
|
|
93
|
+
return (rows || []).map(mapKeyRow);
|
|
94
|
+
}
|
|
95
|
+
static async prune(db, tableName = exports.DEFAULT_KEYS_TABLE, options = {}) {
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
let deletedCount = 0;
|
|
98
|
+
if (options.revokedOlderThanMs !== undefined) {
|
|
99
|
+
const cutoff = now - options.revokedOlderThanMs;
|
|
100
|
+
await db.execute(`DELETE FROM ${tableName} WHERE revoked = 1 AND revoked_at IS NOT NULL AND revoked_at <= ?;`, [cutoff]);
|
|
101
|
+
}
|
|
102
|
+
if (options.expiredOlderThanMs !== undefined) {
|
|
103
|
+
const cutoff = now - options.expiredOlderThanMs;
|
|
104
|
+
await db.execute(`DELETE FROM ${tableName} WHERE expires_at IS NOT NULL AND expires_at <= ?;`, [cutoff]);
|
|
105
|
+
}
|
|
106
|
+
return deletedCount;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
exports.KeyStore = KeyStore;
|
|
@@ -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
|
+
}
|