@mdsiha369/adonis-mercure 2.1.2
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/LICENSE.md +9 -0
- package/README.md +274 -0
- package/build/bin/test.d.ts +1 -0
- package/build/bin/test.js +8 -0
- package/build/configure.d.ts +2 -0
- package/build/configure.js +34 -0
- package/build/index.d.ts +9 -0
- package/build/index.js +14 -0
- package/build/providers/mercure_provider.d.ts +7 -0
- package/build/providers/mercure_provider.js +18 -0
- package/build/services/mercure.d.ts +3 -0
- package/build/services/mercure.js +6 -0
- package/build/src/define_config.d.ts +2 -0
- package/build/src/define_config.js +11 -0
- package/build/src/errors/config_error.d.ts +4 -0
- package/build/src/errors/config_error.js +7 -0
- package/build/src/errors/mercure_error.d.ts +3 -0
- package/build/src/errors/mercure_error.js +6 -0
- package/build/src/errors/publish_error.d.ts +6 -0
- package/build/src/errors/publish_error.js +11 -0
- package/build/src/errors/timeout_error.d.ts +5 -0
- package/build/src/errors/timeout_error.js +9 -0
- package/build/src/fake_mercure.d.ts +18 -0
- package/build/src/fake_mercure.js +52 -0
- package/build/src/mercure.d.ts +9 -0
- package/build/src/mercure.js +42 -0
- package/build/src/publisher.d.ts +6 -0
- package/build/src/publisher.js +54 -0
- package/build/src/token_generator.d.ts +7 -0
- package/build/src/token_generator.js +24 -0
- package/build/src/types/extended.d.ts +6 -0
- package/build/src/types/extended.js +1 -0
- package/build/src/types/main.d.ts +24 -0
- package/build/src/types/main.js +1 -0
- package/build/stubs/config/mercure.stub +15 -0
- package/build/stubs/main.d.ts +5 -0
- package/build/stubs/main.js +7 -0
- package/build/tests/fake_mercure.spec.d.ts +1 -0
- package/build/tests/fake_mercure.spec.js +76 -0
- package/build/tests/mercure.spec.d.ts +1 -0
- package/build/tests/mercure.spec.js +84 -0
- package/build/tests/publisher.spec.d.ts +1 -0
- package/build/tests/publisher.spec.js +101 -0
- package/build/tests/token_generator.spec.d.ts +1 -0
- package/build/tests/token_generator.spec.js +29 -0
- package/configure.ts +41 -0
- package/index.ts +14 -0
- package/package.json +115 -0
- package/providers/mercure_provider.ts +23 -0
- package/src/define_config.ts +18 -0
- package/src/errors/config_error.ts +8 -0
- package/src/errors/mercure_error.ts +6 -0
- package/src/errors/publish_error.ts +14 -0
- package/src/errors/timeout_error.ts +8 -0
- package/src/fake_mercure.ts +74 -0
- package/src/mercure.ts +54 -0
- package/src/publisher.ts +62 -0
- package/src/token_generator.ts +29 -0
- package/src/types/extended.ts +7 -0
- package/src/types/main.ts +31 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { MercureConfigError } from './errors/config_error.js'
|
|
2
|
+
import type { MercureConfig } from './types/main.js'
|
|
3
|
+
|
|
4
|
+
const ALLOWED_ALGORITHMS = ['HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512'] as const
|
|
5
|
+
|
|
6
|
+
export function defineConfig<T extends MercureConfig>(config: T): T {
|
|
7
|
+
if (!ALLOWED_ALGORITHMS.includes(config.jwt.alg as (typeof ALLOWED_ALGORITHMS)[number])) {
|
|
8
|
+
throw new MercureConfigError(
|
|
9
|
+
`Algorithm "${config.jwt.alg}" is not allowed. Use one of: ${ALLOWED_ALGORITHMS.join(', ')}`
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (config.jwt.secret.length < 32) {
|
|
14
|
+
throw new MercureConfigError('jwt.secret must be at least 32 characters long')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return config
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { MercureError } from './mercure_error.js'
|
|
2
|
+
|
|
3
|
+
export class MercurePublishError extends MercureError {
|
|
4
|
+
readonly responseBody: string
|
|
5
|
+
|
|
6
|
+
constructor(
|
|
7
|
+
readonly statusCode: number,
|
|
8
|
+
responseBody: string
|
|
9
|
+
) {
|
|
10
|
+
super(`Mercure hub returned ${statusCode}`)
|
|
11
|
+
this.name = 'MercurePublishError'
|
|
12
|
+
this.responseBody = responseBody.slice(0, 200)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { MercureContract, SendOptions } from './types/main.js'
|
|
2
|
+
|
|
3
|
+
export interface SentMessage {
|
|
4
|
+
topics: string[]
|
|
5
|
+
data: Record<string, unknown>
|
|
6
|
+
options: SendOptions
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class FakeMercure implements MercureContract {
|
|
10
|
+
#sent: SentMessage[] = []
|
|
11
|
+
#tokens: Array<Record<string, unknown>> = []
|
|
12
|
+
|
|
13
|
+
async generate(payload: Record<string, unknown>): Promise<string> {
|
|
14
|
+
this.#tokens.push(payload)
|
|
15
|
+
return 'fake-token'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async generateSubscribeToken(topics: string[]): Promise<string> {
|
|
19
|
+
return this.generate({ subscribe: topics })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async send(
|
|
23
|
+
topics: string | string[],
|
|
24
|
+
data: Record<string, unknown> = {},
|
|
25
|
+
isPrivateOrOptions: boolean | SendOptions = false
|
|
26
|
+
): Promise<Response> {
|
|
27
|
+
const topicList = Array.isArray(topics) ? topics : [topics]
|
|
28
|
+
const options: SendOptions =
|
|
29
|
+
typeof isPrivateOrOptions === 'boolean' ? { private: isPrivateOrOptions } : isPrivateOrOptions
|
|
30
|
+
|
|
31
|
+
this.#sent.push({ topics: topicList, data, options })
|
|
32
|
+
return new Response('1', { status: 200 })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async ping(): Promise<boolean> {
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
assertSent(topic: string, data?: Record<string, unknown>): void {
|
|
40
|
+
const match = this.#sent.find((msg) => {
|
|
41
|
+
if (!msg.topics.includes(topic)) return false
|
|
42
|
+
if (!data) return true
|
|
43
|
+
return JSON.stringify(msg.data) === JSON.stringify(data)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
if (!match) {
|
|
47
|
+
const found = this.#sent.map((m) => m.topics.join(', ')).join('; ') || 'nothing'
|
|
48
|
+
throw new Error(`Expected a message on topic "${topic}" but found: [${found}]`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
assertNotSent(topic: string): void {
|
|
53
|
+
const match = this.#sent.find((msg) => msg.topics.includes(topic))
|
|
54
|
+
if (match) {
|
|
55
|
+
throw new Error(`Expected no message on topic "${topic}" but one was found`)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
assertNothingSent(): void {
|
|
60
|
+
if (this.#sent.length > 0) {
|
|
61
|
+
const topics = this.#sent.map((m) => m.topics.join(', ')).join('; ')
|
|
62
|
+
throw new Error(`Expected nothing to be sent but found messages on: [${topics}]`)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getSent(): SentMessage[] {
|
|
67
|
+
return [...this.#sent]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
clear(): void {
|
|
71
|
+
this.#sent = []
|
|
72
|
+
this.#tokens = []
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/mercure.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { TokenGenerator } from './token_generator.js'
|
|
2
|
+
import { Publisher } from './publisher.js'
|
|
3
|
+
import type { MercureConfig, MercureContract, SendOptions } from './types/main.js'
|
|
4
|
+
|
|
5
|
+
const DEFAULT_TIMEOUT = 5000
|
|
6
|
+
|
|
7
|
+
export class Mercure implements MercureContract {
|
|
8
|
+
#config: MercureConfig
|
|
9
|
+
#tokenGenerator: TokenGenerator
|
|
10
|
+
#publisher: Publisher
|
|
11
|
+
|
|
12
|
+
constructor(config: MercureConfig) {
|
|
13
|
+
this.#config = config
|
|
14
|
+
this.#tokenGenerator = new TokenGenerator(config)
|
|
15
|
+
this.#publisher = new Publisher(config)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
generate(payload: Record<string, unknown>): Promise<string> {
|
|
19
|
+
return this.#tokenGenerator.generate(payload)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
generateSubscribeToken(topics: string[]): Promise<string> {
|
|
23
|
+
return this.#tokenGenerator.generateSubscribeToken(topics)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
send(
|
|
27
|
+
topics: string | string[],
|
|
28
|
+
data: Record<string, unknown> = {},
|
|
29
|
+
isPrivateOrOptions: boolean | SendOptions = false
|
|
30
|
+
): Promise<Response> {
|
|
31
|
+
const options: SendOptions =
|
|
32
|
+
typeof isPrivateOrOptions === 'boolean' ? { private: isPrivateOrOptions } : isPrivateOrOptions
|
|
33
|
+
|
|
34
|
+
return this.#publisher.publish(topics, data, options)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async ping(): Promise<boolean> {
|
|
38
|
+
const timeout = this.#config.http?.timeout ?? DEFAULT_TIMEOUT
|
|
39
|
+
const controller = timeout > 0 ? new AbortController() : null
|
|
40
|
+
const timer = controller ? setTimeout(() => controller.abort(), timeout) : null
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await fetch(this.#config.endpoint, {
|
|
44
|
+
method: 'HEAD',
|
|
45
|
+
signal: controller?.signal,
|
|
46
|
+
})
|
|
47
|
+
return true
|
|
48
|
+
} catch {
|
|
49
|
+
return false
|
|
50
|
+
} finally {
|
|
51
|
+
if (timer) clearTimeout(timer)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/publisher.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { MercurePublishError } from './errors/publish_error.js'
|
|
2
|
+
import { MercureTimeoutError } from './errors/timeout_error.js'
|
|
3
|
+
import type { MercureConfig, SendOptions } from './types/main.js'
|
|
4
|
+
|
|
5
|
+
const DEFAULT_TIMEOUT = 5000
|
|
6
|
+
|
|
7
|
+
export class Publisher {
|
|
8
|
+
#config: MercureConfig
|
|
9
|
+
|
|
10
|
+
constructor(config: MercureConfig) {
|
|
11
|
+
this.#config = config
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async publish(
|
|
15
|
+
topics: string | string[],
|
|
16
|
+
data: Record<string, unknown> = {},
|
|
17
|
+
options: SendOptions = {}
|
|
18
|
+
): Promise<Response> {
|
|
19
|
+
const topicList = Array.isArray(topics) ? topics : [topics]
|
|
20
|
+
const form = new URLSearchParams()
|
|
21
|
+
|
|
22
|
+
for (const topic of topicList) {
|
|
23
|
+
form.append('topic', topic)
|
|
24
|
+
}
|
|
25
|
+
form.append('data', JSON.stringify(data))
|
|
26
|
+
|
|
27
|
+
if (options.private) form.append('private', 'on')
|
|
28
|
+
if (options.id) form.append('id', options.id)
|
|
29
|
+
if (options.type) form.append('type', options.type)
|
|
30
|
+
if (options.retry !== undefined) form.append('retry', String(options.retry))
|
|
31
|
+
|
|
32
|
+
const timeout = this.#config.http?.timeout ?? DEFAULT_TIMEOUT
|
|
33
|
+
const controller = timeout > 0 ? new AbortController() : null
|
|
34
|
+
const timer = controller ? setTimeout(() => controller.abort(), timeout) : null
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetch(this.#config.endpoint, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: {
|
|
40
|
+
'Authorization': `Bearer ${this.#config.adminToken}`,
|
|
41
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
42
|
+
},
|
|
43
|
+
body: form.toString(),
|
|
44
|
+
signal: controller?.signal,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const body = await response.text()
|
|
49
|
+
throw new MercurePublishError(response.status, body)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return response
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if ((err as Error)?.name === 'AbortError') {
|
|
55
|
+
throw new MercureTimeoutError(timeout)
|
|
56
|
+
}
|
|
57
|
+
throw err
|
|
58
|
+
} finally {
|
|
59
|
+
if (timer) clearTimeout(timer)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import jws from 'jws'
|
|
2
|
+
import type { MercureConfig } from './types/main.js'
|
|
3
|
+
|
|
4
|
+
export class TokenGenerator {
|
|
5
|
+
#config: MercureConfig
|
|
6
|
+
|
|
7
|
+
constructor(config: MercureConfig) {
|
|
8
|
+
this.#config = config
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
generate(payload: Record<string, unknown>): Promise<string> {
|
|
12
|
+
return new Promise<string>((resolve, reject) => {
|
|
13
|
+
jws
|
|
14
|
+
.createSign({
|
|
15
|
+
payload: { mercure: payload },
|
|
16
|
+
secret: this.#config.jwt.secret,
|
|
17
|
+
header: { alg: this.#config.jwt.alg },
|
|
18
|
+
})
|
|
19
|
+
.on('error', reject)
|
|
20
|
+
.on('done', resolve)
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// WARNING: never pass user-controlled topics directly — a wildcard topic ('*')
|
|
25
|
+
// would grant the token holder access to all private events.
|
|
26
|
+
generateSubscribeToken(topics: string[]): Promise<string> {
|
|
27
|
+
return this.generate({ subscribe: topics })
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Algorithm } from 'jws'
|
|
2
|
+
|
|
3
|
+
export interface MercureConfig {
|
|
4
|
+
endpoint: string
|
|
5
|
+
adminToken: string
|
|
6
|
+
jwt: {
|
|
7
|
+
alg: Algorithm
|
|
8
|
+
secret: string
|
|
9
|
+
}
|
|
10
|
+
http?: {
|
|
11
|
+
timeout?: number
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SendOptions {
|
|
16
|
+
private?: boolean
|
|
17
|
+
id?: string
|
|
18
|
+
type?: string
|
|
19
|
+
retry?: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface MercureContract {
|
|
23
|
+
generate(payload: Record<string, unknown>): Promise<string>
|
|
24
|
+
generateSubscribeToken(topics: string[]): Promise<string>
|
|
25
|
+
send(
|
|
26
|
+
topics: string | string[],
|
|
27
|
+
data?: Record<string, unknown>,
|
|
28
|
+
options?: boolean | SendOptions
|
|
29
|
+
): Promise<Response>
|
|
30
|
+
ping(): Promise<boolean>
|
|
31
|
+
}
|