@sockethub/data-layer 1.0.0-alpha.3 → 1.0.0-alpha.5

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.
@@ -1,76 +1,124 @@
1
- import SecureStore from 'secure-store-redis';
2
- import debug, {Debugger} from 'debug';
3
- import {IActivityStream, CallbackInterface} from "@sockethub/schemas";
4
- import crypto from "@sockethub/crypto";
5
- import {RedisConfigProps, RedisConfigUrl} from "./types";
1
+ import { crypto } from "@sockethub/crypto";
2
+ import type { CredentialsObject } from "@sockethub/schemas";
3
+ import debug, { type Debugger } from "debug";
4
+ import SecureStore from "secure-store-redis";
5
+
6
+ import type { RedisConfig } from "./types.js";
7
+
8
+ export interface CredentialsStoreInterface {
9
+ get(
10
+ actor: string,
11
+ credentialsHash: string | undefined,
12
+ ): Promise<CredentialsObject | undefined>;
13
+ save(actor: string, creds: CredentialsObject): Promise<number>;
14
+ }
15
+
16
+ export async function verifySecureStore(config: RedisConfig): Promise<void> {
17
+ const log = debug("sockethub:data-layer:credentials-store");
18
+ const ss = new SecureStore({
19
+ redis: config,
20
+ });
21
+ await ss.init();
22
+ await ss.disconnect();
23
+ log("redis connection verified");
24
+ }
6
25
 
7
26
  /**
8
- * Encapsulates the storing and fetching of credential objects.
27
+ * Secure, encrypted storage for user credentials with session-based isolation.
28
+ *
29
+ * Provides automatic encryption/decryption of credential objects stored in Redis,
30
+ * ensuring that sensitive authentication data is never stored in plaintext.
31
+ * Each session gets its own isolated credential store.
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * const store = new CredentialsStore('session123', secret, redisConfig);
36
+ *
37
+ * // Store credentials
38
+ * await store.save('user@example.com', {
39
+ * username: 'user',
40
+ * password: 'secret',
41
+ * server: 'irc.freenode.net'
42
+ * });
43
+ *
44
+ * // Retrieve credentials
45
+ * const creds = await store.get('user@example.com', credentialsHash);
46
+ * ```
9
47
  */
10
- export default class CredentialsStore {
11
- readonly uid: string;
12
- private readonly store: SecureStore;
13
- private readonly log: Debugger;
48
+ export class CredentialsStore implements CredentialsStoreInterface {
49
+ readonly uid: string;
50
+ store: SecureStore;
51
+ objectHash: (o: unknown) => string;
52
+ private readonly log: Debugger;
14
53
 
15
- /**
16
- * @param parentId - The ID of the parent instance (eg. sockethub itself)
17
- * @param sessionId - The ID of the session (socket.io connection)
18
- * @param secret - The encryption secret (parent + session secrets)
19
- * @param redisConfig - Connect info for redis
20
- */
21
- constructor(
22
- parentId: string,
23
- sessionId: string,
24
- secret: string,
25
- redisConfig: RedisConfigProps | RedisConfigUrl
26
- ) {
27
- this.uid = `sockethub:data-layer:credentials-store:${parentId}:${sessionId}`;
28
- this.store = new SecureStore({
29
- namespace: this.uid,
30
- secret: secret,
31
- redis: redisConfig
32
- });
33
- this.log = debug(this.uid);
34
- }
54
+ /**
55
+ * Creates a new CredentialsStore instance.
56
+ *
57
+ * @param parentId - Unique identifier for the parent instance (e.g. server ID)
58
+ * @param sessionId - Client session identifier for credential isolation
59
+ * @param secret - 32-character encryption secret for credential security
60
+ * @param redisConfig - Redis connection configuration
61
+ * @throws Error if secret is not exactly 32 characters
62
+ */
63
+ constructor(
64
+ parentId: string,
65
+ sessionId: string,
66
+ secret: string,
67
+ redisConfig: RedisConfig,
68
+ ) {
69
+ if (secret.length !== 32) {
70
+ throw new Error(
71
+ "CredentialsStore secret must be 32 chars in length",
72
+ );
73
+ }
74
+ this.uid = `sockethub:data-layer:credentials-store:${parentId}:${sessionId}`;
75
+ this.log = debug(this.uid);
76
+ this.initCrypto();
77
+ this.initSecureStore(secret, redisConfig);
78
+ this.log("initialized");
79
+ }
35
80
 
36
- /**
37
- * Gets the credentials for a given actor ID
38
- * @param actor
39
- * @param credentialHash
40
- */
41
- async get(actor: string, credentialHash: string): Promise<IActivityStream> {
42
- this.log(`get credentials for ${actor}`);
43
- return new Promise((resolve, reject) => {
44
- this.store.get(actor, (err, credentials) => {
45
- if (err) { return reject(err.toString()); }
46
- if (!credentials) { return resolve(undefined); }
81
+ initCrypto() {
82
+ this.objectHash = crypto.objectHash;
83
+ }
47
84
 
48
- if (credentialHash) {
49
- if (credentialHash !== crypto.objectHash(credentials.object)) {
50
- return reject(
51
- `provided credentials do not match existing platform instance for actor ${actor}`);
52
- }
85
+ initSecureStore(secret: string, redisConfig: RedisConfig) {
86
+ this.store = new SecureStore({
87
+ uid: this.uid,
88
+ secret: secret,
89
+ redis: redisConfig,
90
+ });
91
+ }
92
+
93
+ /**
94
+ * Gets the credentials for a given actor ID
95
+ * @param actor
96
+ * @param credentialsHash - Optional hash to validate credentials. If undefined, validation is skipped.
97
+ */
98
+ async get(
99
+ actor: string,
100
+ credentialsHash: string | undefined,
101
+ ): Promise<CredentialsObject> {
102
+ this.log(`get credentials for ${actor}`);
103
+ const credentials: CredentialsObject = await this.store.get(actor);
104
+ if (!credentials) {
105
+ throw new Error(`credentials not found for ${actor}`);
53
106
  }
54
- return resolve(credentials);
55
- });
56
- });
57
- }
58
107
 
59
- /**
60
- * Saves the credentials for a given actor ID
61
- * @param actor
62
- * @param creds
63
- * @param done
64
- */
65
- save(actor: string, creds: IActivityStream, done: CallbackInterface): void {
66
- this.store.save(actor, creds, (err) => {
67
- if (err) {
68
- this.log('error saving credentials to store ' + err);
69
- return done(err);
70
- } else {
71
- this.log(`credentials encrypted and saved`);
72
- return done();
73
- }
74
- });
75
- }
76
- }
108
+ if (credentialsHash) {
109
+ if (credentialsHash !== this.objectHash(credentials.object)) {
110
+ throw new Error(`invalid credentials for ${actor}`);
111
+ }
112
+ }
113
+ return credentials;
114
+ }
115
+
116
+ /**
117
+ * Saves the credentials for a given actor ID
118
+ * @param actor
119
+ * @param creds
120
+ */
121
+ async save(actor: string, creds: CredentialsObject): Promise<number> {
122
+ return this.store.save(actor, creds);
123
+ }
124
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,27 @@
1
- export {default as CredentialsStore} from "./credentials-store";
2
- export {default as JobQueue} from "./job-queue";
3
- export * from "./types";
1
+ import debug from "debug";
2
+
3
+ import {
4
+ CredentialsStore,
5
+ type CredentialsStoreInterface,
6
+ verifySecureStore,
7
+ } from "./credentials-store.js";
8
+ import { JobQueue, verifyJobQueue } from "./job-queue.js";
9
+ import { JobWorker } from "./job-worker.js";
10
+ export * from "./types.js";
11
+ import type { RedisConfig } from "./types.js";
12
+
13
+ const log = debug("sockethub:data-layer");
14
+
15
+ async function redisCheck(config: RedisConfig): Promise<void> {
16
+ log(`checking redis connection ${config.url}`);
17
+ await verifySecureStore(config);
18
+ await verifyJobQueue(config);
19
+ }
20
+
21
+ export {
22
+ redisCheck,
23
+ JobQueue,
24
+ JobWorker,
25
+ CredentialsStore,
26
+ type CredentialsStoreInterface,
27
+ };
@@ -0,0 +1,58 @@
1
+ import EventEmitter from "node:events";
2
+ import IORedis, { type Redis } from "ioredis";
3
+
4
+ import { crypto, type Crypto } from "@sockethub/crypto";
5
+ import type { ActivityStream } from "@sockethub/schemas";
6
+
7
+ import type { JobDataDecrypted, JobEncrypted, RedisConfig } from "./types.js";
8
+
9
+ export function createIORedisConnection(config: RedisConfig): Redis {
10
+ return new IORedis(config.url, {
11
+ enableOfflineQueue: false,
12
+ maxRetriesPerRequest: null,
13
+ });
14
+ }
15
+
16
+ export class JobBase extends EventEmitter {
17
+ protected crypto: Crypto;
18
+ private readonly secret: string;
19
+
20
+ constructor(secret: string) {
21
+ super();
22
+ if (secret.length !== 32) {
23
+ throw new Error(
24
+ `secret must be a 32 char string, length: ${secret.length}`,
25
+ );
26
+ }
27
+ this.secret = secret;
28
+ this.initCrypto();
29
+ }
30
+
31
+ initCrypto() {
32
+ this.crypto = crypto;
33
+ }
34
+
35
+ disconnectBase() {
36
+ this.removeAllListeners();
37
+ }
38
+
39
+ /**
40
+ * @param job
41
+ * @private
42
+ */
43
+ protected decryptJobData(job: JobEncrypted): JobDataDecrypted {
44
+ return {
45
+ title: job.data.title,
46
+ msg: this.decryptActivityStream(job.data.msg) as ActivityStream,
47
+ sessionId: job.data.sessionId,
48
+ };
49
+ }
50
+
51
+ protected decryptActivityStream(msg: string): ActivityStream {
52
+ return this.crypto.decrypt(msg, this.secret);
53
+ }
54
+
55
+ protected encryptActivityStream(msg: ActivityStream): string {
56
+ return this.crypto.encrypt(msg, this.secret);
57
+ }
58
+ }