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,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;
|
|
@@ -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;
|