@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.
- package/API.md +23 -0
- package/QUEUE.md +130 -0
- package/README.md +114 -0
- package/package.json +22 -43
- package/src/credentials-store.test.ts +140 -123
- package/src/credentials-store.ts +116 -68
- package/src/index.ts +27 -3
- package/src/job-base.ts +58 -0
- package/src/job-queue.test.ts +265 -237
- package/src/job-queue.ts +210 -131
- package/src/job-worker.test.ts +128 -0
- package/src/job-worker.ts +97 -0
- package/src/types.ts +21 -23
- package/typedoc.json +11 -0
- package/dist/credentials-store.d.ts +0 -31
- package/dist/credentials-store.js +0 -64
- package/dist/credentials-store.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -41
- package/dist/index.js.map +0 -1
- package/dist/job-queue.d.ts +0 -32
- package/dist/job-queue.js +0 -129
- package/dist/job-queue.js.map +0 -1
- package/dist/store.d.ts +0 -3
- package/dist/store.js +0 -17
- package/dist/store.js.map +0 -1
- package/dist/types.d.ts +0 -29
- package/dist/types.js +0 -3
- package/dist/types.js.map +0 -1
package/src/credentials-store.ts
CHANGED
|
@@ -1,76 +1,124 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
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
|
-
*
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
+
};
|
package/src/job-base.ts
ADDED
|
@@ -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
|
+
}
|