@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.
Files changed (60) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +274 -0
  3. package/build/bin/test.d.ts +1 -0
  4. package/build/bin/test.js +8 -0
  5. package/build/configure.d.ts +2 -0
  6. package/build/configure.js +34 -0
  7. package/build/index.d.ts +9 -0
  8. package/build/index.js +14 -0
  9. package/build/providers/mercure_provider.d.ts +7 -0
  10. package/build/providers/mercure_provider.js +18 -0
  11. package/build/services/mercure.d.ts +3 -0
  12. package/build/services/mercure.js +6 -0
  13. package/build/src/define_config.d.ts +2 -0
  14. package/build/src/define_config.js +11 -0
  15. package/build/src/errors/config_error.d.ts +4 -0
  16. package/build/src/errors/config_error.js +7 -0
  17. package/build/src/errors/mercure_error.d.ts +3 -0
  18. package/build/src/errors/mercure_error.js +6 -0
  19. package/build/src/errors/publish_error.d.ts +6 -0
  20. package/build/src/errors/publish_error.js +11 -0
  21. package/build/src/errors/timeout_error.d.ts +5 -0
  22. package/build/src/errors/timeout_error.js +9 -0
  23. package/build/src/fake_mercure.d.ts +18 -0
  24. package/build/src/fake_mercure.js +52 -0
  25. package/build/src/mercure.d.ts +9 -0
  26. package/build/src/mercure.js +42 -0
  27. package/build/src/publisher.d.ts +6 -0
  28. package/build/src/publisher.js +54 -0
  29. package/build/src/token_generator.d.ts +7 -0
  30. package/build/src/token_generator.js +24 -0
  31. package/build/src/types/extended.d.ts +6 -0
  32. package/build/src/types/extended.js +1 -0
  33. package/build/src/types/main.d.ts +24 -0
  34. package/build/src/types/main.js +1 -0
  35. package/build/stubs/config/mercure.stub +15 -0
  36. package/build/stubs/main.d.ts +5 -0
  37. package/build/stubs/main.js +7 -0
  38. package/build/tests/fake_mercure.spec.d.ts +1 -0
  39. package/build/tests/fake_mercure.spec.js +76 -0
  40. package/build/tests/mercure.spec.d.ts +1 -0
  41. package/build/tests/mercure.spec.js +84 -0
  42. package/build/tests/publisher.spec.d.ts +1 -0
  43. package/build/tests/publisher.spec.js +101 -0
  44. package/build/tests/token_generator.spec.d.ts +1 -0
  45. package/build/tests/token_generator.spec.js +29 -0
  46. package/configure.ts +41 -0
  47. package/index.ts +14 -0
  48. package/package.json +115 -0
  49. package/providers/mercure_provider.ts +23 -0
  50. package/src/define_config.ts +18 -0
  51. package/src/errors/config_error.ts +8 -0
  52. package/src/errors/mercure_error.ts +6 -0
  53. package/src/errors/publish_error.ts +14 -0
  54. package/src/errors/timeout_error.ts +8 -0
  55. package/src/fake_mercure.ts +74 -0
  56. package/src/mercure.ts +54 -0
  57. package/src/publisher.ts +62 -0
  58. package/src/token_generator.ts +29 -0
  59. package/src/types/extended.ts +7 -0
  60. 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,8 @@
1
+ import { MercureError } from './mercure_error.js'
2
+
3
+ export class MercureConfigError extends MercureError {
4
+ constructor(message: string) {
5
+ super(message)
6
+ this.name = 'MercureConfigError'
7
+ }
8
+ }
@@ -0,0 +1,6 @@
1
+ export class MercureError extends Error {
2
+ constructor(message: string) {
3
+ super(message)
4
+ this.name = 'MercureError'
5
+ }
6
+ }
@@ -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,8 @@
1
+ import { MercureError } from './mercure_error.js'
2
+
3
+ export class MercureTimeoutError extends MercureError {
4
+ constructor(readonly timeoutMs: number) {
5
+ super(`Request to Mercure hub timed out after ${timeoutMs}ms`)
6
+ this.name = 'MercureTimeoutError'
7
+ }
8
+ }
@@ -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
+ }
@@ -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,7 @@
1
+ import { Mercure } from '../mercure.js'
2
+
3
+ declare module '@adonisjs/core/types' {
4
+ export interface ContainerBindings {
5
+ mercure: Mercure
6
+ }
7
+ }
@@ -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
+ }