@droponair/sdk-js 0.3.0

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.
@@ -0,0 +1,6 @@
1
+ export declare class SessionManager {
2
+ private readonly sharedKeyCache;
3
+ get(peerUserId: string): CryptoKey | null;
4
+ set(peerUserId: string, key: CryptoKey): void;
5
+ clear(): void;
6
+ }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SessionManager = void 0;
4
+ class SessionManager {
5
+ constructor() {
6
+ this.sharedKeyCache = new Map();
7
+ }
8
+ get(peerUserId) {
9
+ return this.sharedKeyCache.get(peerUserId) ?? null;
10
+ }
11
+ set(peerUserId, key) {
12
+ this.sharedKeyCache.set(peerUserId, key);
13
+ }
14
+ clear() {
15
+ this.sharedKeyCache.clear();
16
+ }
17
+ }
18
+ exports.SessionManager = SessionManager;
@@ -0,0 +1,173 @@
1
+ export type DropOnAirEventType = 'SERVER_RECEIVED' | 'DELIVERED' | 'PROCESSED' | 'LIMIT_REACHED' | 'IMPERSONATION_DETECTED' | 'ERROR' | 'CONNECTED' | 'DISCONNECTED' | 'RECONNECTING';
2
+ export interface DropOnAirEvent {
3
+ type: DropOnAirEventType | string;
4
+ reason?: string;
5
+ metadata?: string;
6
+ }
7
+ export interface DecryptedMessage {
8
+ messageId: string;
9
+ fromUserId: string;
10
+ toUserId: string;
11
+ timestamp: number;
12
+ plaintext: string;
13
+ }
14
+ export type MessageCallback = (message: DecryptedMessage) => void;
15
+ export type EventCallback = (event: DropOnAirEvent) => void;
16
+ export interface BroadcastMessage {
17
+ broadcastId: string;
18
+ channelId: string;
19
+ publisherId: string;
20
+ timestamp: number;
21
+ plaintext: string;
22
+ sequenceNumber: number;
23
+ }
24
+ export type BroadcastCallback = (message: BroadcastMessage) => void;
25
+ export type CallEventType = 'CALL_INVITE' | 'CALL_RINGING' | 'CALL_ACCEPTED' | 'CALL_REJECTED' | 'CALL_ENDED' | 'CALL_CANCELLED' | 'CALL_SDP_OFFER' | 'CALL_SDP_ANSWER' | 'CALL_ICE_CANDIDATE' | 'CALL_VIDEO_TOGGLE' | 'CALL_DENIED_LIMIT_REACHED';
26
+ export interface CallEvent {
27
+ type: CallEventType | string;
28
+ callId?: string;
29
+ targetUserId?: string;
30
+ /** Opaque JSON payload: SDP, ICE candidate, or metadata. */
31
+ payload?: string;
32
+ }
33
+ export type CallEventCallback = (event: CallEvent) => void;
34
+ export interface TurnCredentials {
35
+ username: string;
36
+ password: string;
37
+ uri: string;
38
+ ttlSeconds: number;
39
+ }
40
+ export interface GroupInfo {
41
+ groupId: string;
42
+ name: string;
43
+ createdBy: string;
44
+ members: GroupMemberInfo[];
45
+ createdAt: number;
46
+ }
47
+ export interface GroupMemberInfo {
48
+ userId: string;
49
+ role: 'OWNER' | 'ADMIN' | 'MEMBER';
50
+ joinedAt: number;
51
+ }
52
+ export interface DecryptedGroupMessage {
53
+ messageId: string;
54
+ groupId: string;
55
+ fromUserId: string;
56
+ timestamp: number;
57
+ plaintext: string;
58
+ }
59
+ export type GroupMessageCallback = (message: DecryptedGroupMessage) => void;
60
+ export type GroupCallEventType = 'GROUP_CALL_INVITE' | 'GROUP_CALL_RINGING' | 'GROUP_CALL_JOIN' | 'GROUP_CALL_LEAVE' | 'GROUP_CALL_END' | 'GROUP_CALL_ENDED' | 'GROUP_CALL_PARTICIPANT_JOINED' | 'GROUP_CALL_PARTICIPANT_LEFT' | 'GROUP_CALL_ALREADY_ACTIVE' | 'GROUP_CALL_SDP_OFFER' | 'GROUP_CALL_SDP_ANSWER' | 'GROUP_CALL_ICE_CANDIDATE';
61
+ export interface GroupCallEvent {
62
+ type: GroupCallEventType | string;
63
+ callId: string;
64
+ groupId: string;
65
+ targetUserId?: string;
66
+ payload?: string;
67
+ }
68
+ export type GroupCallEventCallback = (event: GroupCallEvent) => void;
69
+ export interface KeyStorageAdapter {
70
+ get(key: string): Promise<string | null>;
71
+ set(key: string, value: string): Promise<void>;
72
+ remove(key: string): Promise<void>;
73
+ }
74
+ export interface InitializeOptions {
75
+ appId: string;
76
+ publicApiKey: string;
77
+ getUserJwt: () => Promise<string>;
78
+ autoConnect?: boolean;
79
+ messagingWsUrl?: string;
80
+ messagingHttpUrl?: string;
81
+ tokenExchangeEndpoint?: string;
82
+ keyDirectoryEndpoint?: string;
83
+ fetchFn?: typeof fetch;
84
+ storage?: KeyStorageAdapter;
85
+ /** When true, verbose diagnostic logs are emitted to console for all SDK operations. */
86
+ debug?: boolean;
87
+ /**
88
+ * Platform identifier forwarded as the X-Platform header on every backend API call
89
+ * (e.g. 'ios' or 'android'). Some backends require this header.
90
+ */
91
+ platformHeader?: string;
92
+ /** API version forwarded as the X-API-Version header on every backend API call (e.g. '1.0.0'). */
93
+ apiVersionHeader?: string;
94
+ /**
95
+ * When true (default), SDK sends PROCESSED ack immediately after decrypting inbound messages.
96
+ * Set false to let app decide when a message is actually seen/read and call `ack(messageId)` manually.
97
+ */
98
+ autoAckIncomingMessages?: boolean;
99
+ }
100
+ export interface DropOnAirClient {
101
+ connect(): Promise<void>;
102
+ disconnect(): void;
103
+ sendMessage(toUserId: string, plaintextMessage: string): Promise<{
104
+ messageId: string;
105
+ }>;
106
+ onMessage(callback: MessageCallback): () => void;
107
+ onEvent(callback: EventCallback): () => void;
108
+ ack(messageId: string): Promise<void>;
109
+ /** Send a cleartext message to a user (no encryption). */
110
+ sendCleartextMessage(toUserId: string, plaintext: string): Promise<{
111
+ messageId: string;
112
+ }>;
113
+ /** Subscribe to a broadcast channel via REST. */
114
+ subscribeBroadcast(channelId: string): Promise<void>;
115
+ /** Unsubscribe from a broadcast channel via REST. */
116
+ unsubscribeBroadcast(channelId: string): Promise<void>;
117
+ /** Publish a cleartext message to a broadcast channel via WebSocket. */
118
+ publishBroadcast(channelId: string, plaintext: string): Promise<{
119
+ broadcastId: string;
120
+ }>;
121
+ /** Register a listener for broadcast notifications. Returns an unsubscribe function. */
122
+ onBroadcast(callback: BroadcastCallback): () => void;
123
+ /** Initiate an outgoing call. Resolves with the server-assigned callId once CALL_RINGING is received. */
124
+ startCall(targetUserId: string): Promise<string>;
125
+ /** Accept an incoming call identified by callId. */
126
+ acceptCall(callId: string): Promise<void>;
127
+ /** Reject an incoming call. */
128
+ rejectCall(callId: string): Promise<void>;
129
+ /** End an active call (or cancel an unanswered invite). */
130
+ endCall(callId: string): Promise<void>;
131
+ /** Toggle local video on/off for an active call. */
132
+ toggleVideo(callId: string, enabled: boolean): void;
133
+ /**
134
+ * Send a raw call signaling frame to the peer.
135
+ * Used by the application layer to relay SDP offers/answers and ICE candidates.
136
+ * The payload is forwarded opaquely, the server does NOT inspect or store it.
137
+ */
138
+ sendCallSignal(type: 'CALL_SDP_OFFER' | 'CALL_SDP_ANSWER' | 'CALL_ICE_CANDIDATE', callId: string, payload: string): void;
139
+ /** Register a listener for all incoming call events. Returns an unsubscribe function. */
140
+ onCallEvent(callback: CallEventCallback): () => void;
141
+ /** Fetch short-lived TURN credentials for ICE negotiation. */
142
+ fetchTurnCredentials(): Promise<TurnCredentials>;
143
+ /** Create a new group with optional initial members. */
144
+ createGroup(name: string, memberUserIds?: string[]): Promise<GroupInfo>;
145
+ /** List all groups the current user is a member of. */
146
+ listGroups(): Promise<GroupInfo[]>;
147
+ /** Get details of a specific group. */
148
+ getGroup(groupId: string): Promise<GroupInfo>;
149
+ /** Add members to a group (requires OWNER/ADMIN role). */
150
+ addGroupMembers(groupId: string, userIds: string[]): Promise<GroupInfo>;
151
+ /** Remove a member from a group (OWNER/ADMIN can remove others; anyone can leave). */
152
+ removeGroupMember(groupId: string, userId: string): Promise<GroupInfo>;
153
+ /** Delete a group (requires OWNER role). */
154
+ deleteGroup(groupId: string): Promise<void>;
155
+ /** Send a cleartext message to a group. */
156
+ sendGroupMessage(groupId: string, plaintext: string): Promise<{
157
+ messageId: string;
158
+ }>;
159
+ /** Register a listener for incoming group messages. Returns an unsubscribe function. */
160
+ onGroupMessage(callback: GroupMessageCallback): () => void;
161
+ /** Initiate a group call. Returns the callId. */
162
+ startGroupCall(groupId: string): Promise<string>;
163
+ /** Join an active group call. */
164
+ joinGroupCall(callId: string, groupId: string): Promise<void>;
165
+ /** Leave a group call. */
166
+ leaveGroupCall(callId: string): Promise<void>;
167
+ /** End a group call for all participants. */
168
+ endGroupCall(callId: string): Promise<void>;
169
+ /** Send a signaling frame to a specific peer in a group call. */
170
+ sendGroupCallSignal(type: 'GROUP_CALL_SDP_OFFER' | 'GROUP_CALL_SDP_ANSWER' | 'GROUP_CALL_ICE_CANDIDATE', callId: string, groupId: string, targetUserId: string, payload: string): void;
171
+ /** Register a listener for group call events. Returns an unsubscribe function. */
172
+ onGroupCallEvent(callback: GroupCallEventCallback): () => void;
173
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,28 @@
1
+ import { SessionManager } from '../core/session-manager';
2
+ import { KeyStorageAdapter } from '../core/types';
3
+ export declare class CryptoService {
4
+ private readonly storage;
5
+ private readonly sessionManager;
6
+ constructor(storage: KeyStorageAdapter, sessionManager: SessionManager);
7
+ generateIdentity(): Promise<{
8
+ publicKey: string;
9
+ }>;
10
+ getOrCreateIdentity(): Promise<{
11
+ publicKey: string;
12
+ }>;
13
+ deriveSharedSecret(peerUserId: string, peerPublicKeyBase64: string, localUserId: string): Promise<CryptoKey>;
14
+ encrypt(plaintext: string, symmetricKey: CryptoKey, aadContext: {
15
+ messageId: string;
16
+ senderId: string;
17
+ recipientId: string;
18
+ timestamp: number;
19
+ }): Promise<Uint8Array>;
20
+ decrypt(encryptedPayload: Uint8Array, symmetricKey: CryptoKey, aadContext: {
21
+ messageId: string;
22
+ senderId: string;
23
+ recipientId: string;
24
+ timestamp: number;
25
+ }): Promise<string>;
26
+ private deriveAesKey;
27
+ private buildAad;
28
+ }
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.CryptoService = void 0;
7
+ const tweetnacl_1 = __importDefault(require("tweetnacl"));
8
+ const bytes_1 = require("../core/bytes");
9
+ const payload_format_1 = require("./payload-format");
10
+ const STORAGE_PRIVATE = 'droponair.identity.privateKey.v1';
11
+ const STORAGE_PUBLIC = 'droponair.identity.publicKey.v1';
12
+ function asBufferSource(data) {
13
+ return new Uint8Array(data);
14
+ }
15
+ class CryptoService {
16
+ constructor(storage, sessionManager) {
17
+ this.storage = storage;
18
+ this.sessionManager = sessionManager;
19
+ }
20
+ async generateIdentity() {
21
+ const keyPair = tweetnacl_1.default.box.keyPair();
22
+ await this.storage.set(STORAGE_PRIVATE, (0, bytes_1.toBase64)(keyPair.secretKey));
23
+ await this.storage.set(STORAGE_PUBLIC, (0, bytes_1.toBase64)(keyPair.publicKey));
24
+ return { publicKey: (0, bytes_1.toBase64)(keyPair.publicKey) };
25
+ }
26
+ async getOrCreateIdentity() {
27
+ const existingPublic = await this.storage.get(STORAGE_PUBLIC);
28
+ const existingPrivate = await this.storage.get(STORAGE_PRIVATE);
29
+ if (existingPublic && existingPrivate) {
30
+ return { publicKey: existingPublic };
31
+ }
32
+ return this.generateIdentity();
33
+ }
34
+ async deriveSharedSecret(peerUserId, peerPublicKeyBase64, localUserId) {
35
+ // Cache by public key (not userId) to support multi-device, same user may have
36
+ // different public keys on different devices, each producing a different shared secret.
37
+ const cacheKey = peerPublicKeyBase64;
38
+ const cached = this.sessionManager.get(cacheKey);
39
+ if (cached) {
40
+ return cached;
41
+ }
42
+ const privateBase64 = await this.storage.get(STORAGE_PRIVATE);
43
+ if (!privateBase64) {
44
+ throw new Error('Local identity keypair is missing');
45
+ }
46
+ const privateKey = (0, bytes_1.fromBase64)(privateBase64);
47
+ const peerPublicKey = (0, bytes_1.fromBase64)(peerPublicKeyBase64);
48
+ if (peerPublicKey.length !== 32 || privateKey.length !== 32) {
49
+ throw new Error('Invalid X25519 key length');
50
+ }
51
+ const sharedSecret = tweetnacl_1.default.scalarMult(privateKey, peerPublicKey);
52
+ const symmetricKey = await this.deriveAesKey(sharedSecret, localUserId, peerUserId);
53
+ this.sessionManager.set(cacheKey, symmetricKey);
54
+ return symmetricKey;
55
+ }
56
+ async encrypt(plaintext, symmetricKey, aadContext) {
57
+ const nonce = (0, bytes_1.randomBytes)(12);
58
+ const aad = this.buildAad(aadContext);
59
+ const cipherBuffer = await crypto.subtle.encrypt({
60
+ name: 'AES-GCM',
61
+ iv: asBufferSource(nonce),
62
+ additionalData: asBufferSource(aad),
63
+ tagLength: 128
64
+ }, symmetricKey, asBufferSource((0, bytes_1.utf8Encode)(plaintext)));
65
+ return (0, payload_format_1.packEncryptedPayload)({
66
+ nonce,
67
+ ciphertext: new Uint8Array(cipherBuffer)
68
+ });
69
+ }
70
+ async decrypt(encryptedPayload, symmetricKey, aadContext) {
71
+ const unpacked = (0, payload_format_1.unpackEncryptedPayload)(encryptedPayload);
72
+ const aad = this.buildAad(aadContext);
73
+ const plainBuffer = await crypto.subtle.decrypt({
74
+ name: 'AES-GCM',
75
+ iv: asBufferSource(unpacked.nonce),
76
+ additionalData: asBufferSource(aad),
77
+ tagLength: 128
78
+ }, symmetricKey, asBufferSource(unpacked.ciphertext));
79
+ return (0, bytes_1.utf8Decode)(new Uint8Array(plainBuffer));
80
+ }
81
+ async deriveAesKey(sharedSecret, localUserId, peerUserId) {
82
+ const ordering = [localUserId, peerUserId].sort().join(':');
83
+ const info = (0, bytes_1.utf8Encode)(`droponair-e2ee-v1:${ordering}`);
84
+ const salt = (0, bytes_1.utf8Encode)('droponair-e2ee-hkdf-salt-v1');
85
+ const ikm = await crypto.subtle.importKey('raw', asBufferSource(sharedSecret), 'HKDF', false, ['deriveKey']);
86
+ return crypto.subtle.deriveKey({
87
+ name: 'HKDF',
88
+ hash: 'SHA-256',
89
+ salt: asBufferSource(salt),
90
+ info: asBufferSource(info)
91
+ }, ikm, {
92
+ name: 'AES-GCM',
93
+ length: 256
94
+ }, false, ['encrypt', 'decrypt']);
95
+ }
96
+ buildAad(context) {
97
+ return (0, bytes_1.utf8Encode)(`${context.messageId}|${context.senderId}|${context.recipientId}|${context.timestamp}`);
98
+ }
99
+ }
100
+ exports.CryptoService = CryptoService;
@@ -0,0 +1,8 @@
1
+ interface PackedEncryptedPayload {
2
+ nonce: Uint8Array;
3
+ ciphertext: Uint8Array;
4
+ signature?: Uint8Array;
5
+ }
6
+ export declare function packEncryptedPayload(payload: PackedEncryptedPayload): Uint8Array;
7
+ export declare function unpackEncryptedPayload(bytes: Uint8Array): PackedEncryptedPayload;
8
+ export {};
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.packEncryptedPayload = packEncryptedPayload;
4
+ exports.unpackEncryptedPayload = unpackEncryptedPayload;
5
+ const bytes_1 = require("../core/bytes");
6
+ const version_1 = require("../version");
7
+ const VERSION = version_1.PAYLOAD_FORMAT_VERSION;
8
+ const ALGORITHM_AES_256_GCM = 1;
9
+ function uint32Bytes(value) {
10
+ const out = new Uint8Array(4);
11
+ const view = new DataView(out.buffer);
12
+ view.setUint32(0, value, false);
13
+ return out;
14
+ }
15
+ function readUint32(source, offset) {
16
+ const view = new DataView(source.buffer, source.byteOffset + offset, 4);
17
+ return view.getUint32(0, false);
18
+ }
19
+ function packEncryptedPayload(payload) {
20
+ const signature = payload.signature ?? new Uint8Array(0);
21
+ const flags = signature.length > 0 ? 1 : 0;
22
+ const header = new Uint8Array([
23
+ VERSION,
24
+ ALGORITHM_AES_256_GCM,
25
+ payload.nonce.length,
26
+ flags
27
+ ]);
28
+ return (0, bytes_1.concatBytes)(header, uint32Bytes(payload.ciphertext.length), uint32Bytes(signature.length), payload.nonce, payload.ciphertext, signature);
29
+ }
30
+ function unpackEncryptedPayload(bytes) {
31
+ if (bytes.length < 12) {
32
+ throw new Error('Invalid encrypted payload frame');
33
+ }
34
+ const version = bytes[0];
35
+ const algorithm = bytes[1];
36
+ const nonceLength = bytes[2];
37
+ const flags = bytes[3];
38
+ if (version !== VERSION) {
39
+ throw new Error('Unsupported encrypted payload version');
40
+ }
41
+ if (algorithm !== ALGORITHM_AES_256_GCM) {
42
+ throw new Error('Unsupported encrypted payload algorithm');
43
+ }
44
+ const cipherLength = readUint32(bytes, 4);
45
+ const signatureLength = readUint32(bytes, 8);
46
+ const nonceStart = 12;
47
+ const cipherStart = nonceStart + nonceLength;
48
+ const signatureStart = cipherStart + cipherLength;
49
+ const end = signatureStart + signatureLength;
50
+ if (bytes.length < end) {
51
+ throw new Error('Encrypted payload truncated');
52
+ }
53
+ const nonce = bytes.slice(nonceStart, cipherStart);
54
+ const ciphertext = bytes.slice(cipherStart, signatureStart);
55
+ if ((flags & 1) === 1) {
56
+ return {
57
+ nonce,
58
+ ciphertext,
59
+ signature: bytes.slice(signatureStart, end)
60
+ };
61
+ }
62
+ return { nonce, ciphertext };
63
+ }
@@ -0,0 +1,8 @@
1
+ import { InitializeOptions, DropOnAirClient } from './core/types';
2
+ export { SDK_VERSION, PROTOCOL_VERSION, PAYLOAD_FORMAT_VERSION } from './version';
3
+ export declare function initialize(options: InitializeOptions): Promise<DropOnAirClient>;
4
+ export type { InitializeOptions, DropOnAirClient, DropOnAirEvent, MessageCallback, EventCallback, DecryptedMessage, KeyStorageAdapter, CallEvent, CallEventType, CallEventCallback, TurnCredentials, BroadcastMessage, BroadcastCallback, GroupInfo, GroupMemberInfo, DecryptedGroupMessage, GroupMessageCallback, GroupCallEvent, GroupCallEventType, GroupCallEventCallback, } from './core/types';
5
+ declare const _default: {
6
+ initialize: typeof initialize;
7
+ };
8
+ export default _default;
package/dist/index.js ADDED
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PAYLOAD_FORMAT_VERSION = exports.PROTOCOL_VERSION = exports.SDK_VERSION = void 0;
4
+ exports.initialize = initialize;
5
+ const messaging_client_1 = require("./core/messaging-client");
6
+ const session_manager_1 = require("./core/session-manager");
7
+ const crypto_service_1 = require("./crypto/crypto-service");
8
+ const indexeddb_key_storage_1 = require("./storage/indexeddb-key-storage");
9
+ var version_1 = require("./version");
10
+ Object.defineProperty(exports, "SDK_VERSION", { enumerable: true, get: function () { return version_1.SDK_VERSION; } });
11
+ Object.defineProperty(exports, "PROTOCOL_VERSION", { enumerable: true, get: function () { return version_1.PROTOCOL_VERSION; } });
12
+ Object.defineProperty(exports, "PAYLOAD_FORMAT_VERSION", { enumerable: true, get: function () { return version_1.PAYLOAD_FORMAT_VERSION; } });
13
+ function resolveStorage(adapter) {
14
+ if (adapter) {
15
+ return adapter;
16
+ }
17
+ return new indexeddb_key_storage_1.IndexedDbKeyStorage();
18
+ }
19
+ async function initialize(options) {
20
+ const storage = resolveStorage(options.storage);
21
+ const sessionManager = new session_manager_1.SessionManager();
22
+ const cryptoService = new crypto_service_1.CryptoService(storage, sessionManager);
23
+ const client = new messaging_client_1.MessagingClient(options, cryptoService, sessionManager, storage);
24
+ if (options.autoConnect !== false) {
25
+ await client.connect();
26
+ }
27
+ return client;
28
+ }
29
+ exports.default = { initialize };
@@ -0,0 +1,9 @@
1
+ import { KeyStorageAdapter } from '../core/types';
2
+ export declare class IndexedDbKeyStorage implements KeyStorageAdapter {
3
+ private readonly fallback;
4
+ get(key: string): Promise<string | null>;
5
+ set(key: string, value: string): Promise<void>;
6
+ remove(key: string): Promise<void>;
7
+ private isIndexedDbAvailable;
8
+ private openDb;
9
+ }
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IndexedDbKeyStorage = void 0;
4
+ const memory_key_storage_1 = require("./memory-key-storage");
5
+ const DB_NAME = 'droponair_sdk';
6
+ const STORE_NAME = 'secure_keys';
7
+ const DB_VERSION = 1;
8
+ class IndexedDbKeyStorage {
9
+ constructor() {
10
+ this.fallback = new memory_key_storage_1.MemoryKeyStorage();
11
+ }
12
+ async get(key) {
13
+ if (!this.isIndexedDbAvailable()) {
14
+ return this.fallback.get(key);
15
+ }
16
+ const db = await this.openDb();
17
+ return new Promise((resolve, reject) => {
18
+ const tx = db.transaction(STORE_NAME, 'readonly');
19
+ const store = tx.objectStore(STORE_NAME);
20
+ const req = store.get(key);
21
+ req.onsuccess = () => {
22
+ resolve(req.result ?? null);
23
+ };
24
+ req.onerror = () => reject(req.error);
25
+ });
26
+ }
27
+ async set(key, value) {
28
+ if (!this.isIndexedDbAvailable()) {
29
+ await this.fallback.set(key, value);
30
+ return;
31
+ }
32
+ const db = await this.openDb();
33
+ await new Promise((resolve, reject) => {
34
+ const tx = db.transaction(STORE_NAME, 'readwrite');
35
+ tx.objectStore(STORE_NAME).put(value, key);
36
+ tx.oncomplete = () => resolve();
37
+ tx.onerror = () => reject(tx.error);
38
+ });
39
+ }
40
+ async remove(key) {
41
+ if (!this.isIndexedDbAvailable()) {
42
+ await this.fallback.remove(key);
43
+ return;
44
+ }
45
+ const db = await this.openDb();
46
+ await new Promise((resolve, reject) => {
47
+ const tx = db.transaction(STORE_NAME, 'readwrite');
48
+ tx.objectStore(STORE_NAME).delete(key);
49
+ tx.oncomplete = () => resolve();
50
+ tx.onerror = () => reject(tx.error);
51
+ });
52
+ }
53
+ isIndexedDbAvailable() {
54
+ return typeof indexedDB !== 'undefined';
55
+ }
56
+ async openDb() {
57
+ return new Promise((resolve, reject) => {
58
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
59
+ request.onupgradeneeded = () => {
60
+ const db = request.result;
61
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
62
+ db.createObjectStore(STORE_NAME);
63
+ }
64
+ };
65
+ request.onsuccess = () => resolve(request.result);
66
+ request.onerror = () => reject(request.error);
67
+ });
68
+ }
69
+ }
70
+ exports.IndexedDbKeyStorage = IndexedDbKeyStorage;
@@ -0,0 +1,7 @@
1
+ import { KeyStorageAdapter } from '../core/types';
2
+ export declare class MemoryKeyStorage implements KeyStorageAdapter {
3
+ private readonly state;
4
+ get(key: string): Promise<string | null>;
5
+ set(key: string, value: string): Promise<void>;
6
+ remove(key: string): Promise<void>;
7
+ }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MemoryKeyStorage = void 0;
4
+ class MemoryKeyStorage {
5
+ constructor() {
6
+ this.state = new Map();
7
+ }
8
+ async get(key) {
9
+ return this.state.get(key) ?? null;
10
+ }
11
+ async set(key, value) {
12
+ this.state.set(key, value);
13
+ }
14
+ async remove(key) {
15
+ this.state.delete(key);
16
+ }
17
+ }
18
+ exports.MemoryKeyStorage = MemoryKeyStorage;