@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,54 @@
1
+ import { MercurePublishError } from './errors/publish_error.js';
2
+ import { MercureTimeoutError } from './errors/timeout_error.js';
3
+ const DEFAULT_TIMEOUT = 5000;
4
+ export class Publisher {
5
+ #config;
6
+ constructor(config) {
7
+ this.#config = config;
8
+ }
9
+ async publish(topics, data = {}, options = {}) {
10
+ const topicList = Array.isArray(topics) ? topics : [topics];
11
+ const form = new URLSearchParams();
12
+ for (const topic of topicList) {
13
+ form.append('topic', topic);
14
+ }
15
+ form.append('data', JSON.stringify(data));
16
+ if (options.private)
17
+ form.append('private', 'on');
18
+ if (options.id)
19
+ form.append('id', options.id);
20
+ if (options.type)
21
+ form.append('type', options.type);
22
+ if (options.retry !== undefined)
23
+ form.append('retry', String(options.retry));
24
+ const timeout = this.#config.http?.timeout ?? DEFAULT_TIMEOUT;
25
+ const controller = timeout > 0 ? new AbortController() : null;
26
+ const timer = controller ? setTimeout(() => controller.abort(), timeout) : null;
27
+ try {
28
+ const response = await fetch(this.#config.endpoint, {
29
+ method: 'POST',
30
+ headers: {
31
+ 'Authorization': `Bearer ${this.#config.adminToken}`,
32
+ 'Content-Type': 'application/x-www-form-urlencoded',
33
+ },
34
+ body: form.toString(),
35
+ signal: controller?.signal,
36
+ });
37
+ if (!response.ok) {
38
+ const body = await response.text();
39
+ throw new MercurePublishError(response.status, body);
40
+ }
41
+ return response;
42
+ }
43
+ catch (err) {
44
+ if (err?.name === 'AbortError') {
45
+ throw new MercureTimeoutError(timeout);
46
+ }
47
+ throw err;
48
+ }
49
+ finally {
50
+ if (timer)
51
+ clearTimeout(timer);
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,7 @@
1
+ import type { MercureConfig } from './types/main.js';
2
+ export declare class TokenGenerator {
3
+ #private;
4
+ constructor(config: MercureConfig);
5
+ generate(payload: Record<string, unknown>): Promise<string>;
6
+ generateSubscribeToken(topics: string[]): Promise<string>;
7
+ }
@@ -0,0 +1,24 @@
1
+ import jws from 'jws';
2
+ export class TokenGenerator {
3
+ #config;
4
+ constructor(config) {
5
+ this.#config = config;
6
+ }
7
+ generate(payload) {
8
+ return new Promise((resolve, reject) => {
9
+ jws
10
+ .createSign({
11
+ payload: { mercure: payload },
12
+ secret: this.#config.jwt.secret,
13
+ header: { alg: this.#config.jwt.alg },
14
+ })
15
+ .on('error', reject)
16
+ .on('done', resolve);
17
+ });
18
+ }
19
+ // WARNING: never pass user-controlled topics directly — a wildcard topic ('*')
20
+ // would grant the token holder access to all private events.
21
+ generateSubscribeToken(topics) {
22
+ return this.generate({ subscribe: topics });
23
+ }
24
+ }
@@ -0,0 +1,6 @@
1
+ import { Mercure } from '../mercure.js';
2
+ declare module '@adonisjs/core/types' {
3
+ interface ContainerBindings {
4
+ mercure: Mercure;
5
+ }
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import type { Algorithm } from 'jws';
2
+ export interface MercureConfig {
3
+ endpoint: string;
4
+ adminToken: string;
5
+ jwt: {
6
+ alg: Algorithm;
7
+ secret: string;
8
+ };
9
+ http?: {
10
+ timeout?: number;
11
+ };
12
+ }
13
+ export interface SendOptions {
14
+ private?: boolean;
15
+ id?: string;
16
+ type?: string;
17
+ retry?: number;
18
+ }
19
+ export interface MercureContract {
20
+ generate(payload: Record<string, unknown>): Promise<string>;
21
+ generateSubscribeToken(topics: string[]): Promise<string>;
22
+ send(topics: string | string[], data?: Record<string, unknown>, options?: boolean | SendOptions): Promise<Response>;
23
+ ping(): Promise<boolean>;
24
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ {{{
2
+ exports({ to: app.configPath('mercure.ts') })
3
+ }}}
4
+ import env from '#start/env'
5
+ import { defineConfig } from '@mdsiha369/adonis-mercure'
6
+
7
+ export default defineConfig({
8
+ endpoint: env.get('MERCURE_ENDPOINT'),
9
+ adminToken: env.get('MERCURE_ADMIN_JWT'),
10
+
11
+ jwt: {
12
+ alg: 'HS256',
13
+ secret: env.get('MERCURE_JWT_SECRET'),
14
+ },
15
+ })
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Path to the root directory where the stubs are stored. We use
3
+ * this path within commands and the configure hook
4
+ */
5
+ export declare const stubsRoot: string;
@@ -0,0 +1,7 @@
1
+ import { dirname } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ /**
4
+ * Path to the root directory where the stubs are stored. We use
5
+ * this path within commands and the configure hook
6
+ */
7
+ export const stubsRoot = dirname(fileURLToPath(import.meta.url));
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,76 @@
1
+ import { test } from '@japa/runner';
2
+ import { FakeMercure } from '../src/fake_mercure.js';
3
+ test.group('FakeMercure', () => {
4
+ test('records sent messages', async ({ assert }) => {
5
+ const fake = new FakeMercure();
6
+ await fake.send('/orders/1', { status: 'shipped' });
7
+ const sent = fake.getSent();
8
+ assert.lengthOf(sent, 1);
9
+ assert.deepEqual(sent[0].topics, ['/orders/1']);
10
+ assert.deepEqual(sent[0].data, { status: 'shipped' });
11
+ });
12
+ test('assertSent passes when topic matches', async ({ assert }) => {
13
+ const fake = new FakeMercure();
14
+ await fake.send('/topic', { foo: 'bar' });
15
+ assert.doesNotThrow(() => fake.assertSent('/topic'));
16
+ });
17
+ test('assertSent passes when topic and data match', async ({ assert }) => {
18
+ const fake = new FakeMercure();
19
+ await fake.send('/topic', { foo: 'bar' });
20
+ assert.doesNotThrow(() => fake.assertSent('/topic', { foo: 'bar' }));
21
+ });
22
+ test('assertSent throws when topic was not sent', ({ assert }) => {
23
+ const fake = new FakeMercure();
24
+ assert.throws(() => fake.assertSent('/missing'));
25
+ });
26
+ test('assertSent throws when data does not match', async ({ assert }) => {
27
+ const fake = new FakeMercure();
28
+ await fake.send('/topic', { foo: 'bar' });
29
+ assert.throws(() => fake.assertSent('/topic', { foo: 'wrong' }));
30
+ });
31
+ test('assertNotSent passes when topic was never sent', ({ assert }) => {
32
+ const fake = new FakeMercure();
33
+ assert.doesNotThrow(() => fake.assertNotSent('/topic'));
34
+ });
35
+ test('assertNotSent throws when topic was sent', async ({ assert }) => {
36
+ const fake = new FakeMercure();
37
+ await fake.send('/topic', {});
38
+ assert.throws(() => fake.assertNotSent('/topic'));
39
+ });
40
+ test('assertNothingSent passes on fresh instance', ({ assert }) => {
41
+ const fake = new FakeMercure();
42
+ assert.doesNotThrow(() => fake.assertNothingSent());
43
+ });
44
+ test('assertNothingSent throws after a send', async ({ assert }) => {
45
+ const fake = new FakeMercure();
46
+ await fake.send('/topic', {});
47
+ assert.throws(() => fake.assertNothingSent());
48
+ });
49
+ test('clear resets all recorded messages', async ({ assert }) => {
50
+ const fake = new FakeMercure();
51
+ await fake.send('/topic', {});
52
+ fake.clear();
53
+ assert.lengthOf(fake.getSent(), 0);
54
+ assert.doesNotThrow(() => fake.assertNothingSent());
55
+ });
56
+ test('handles boolean true as private option', async ({ assert }) => {
57
+ const fake = new FakeMercure();
58
+ await fake.send('/topic', {}, true);
59
+ const sent = fake.getSent();
60
+ assert.isTrue(sent[0].options.private);
61
+ });
62
+ test('ping always returns true', async ({ assert }) => {
63
+ const fake = new FakeMercure();
64
+ assert.isTrue(await fake.ping());
65
+ });
66
+ test('generate stores the payload and returns fake-token', async ({ assert }) => {
67
+ const fake = new FakeMercure();
68
+ const token = await fake.generate({ subscribe: ['/chat/1'] });
69
+ assert.equal(token, 'fake-token');
70
+ });
71
+ test('generateSubscribeToken returns fake-token', async ({ assert }) => {
72
+ const fake = new FakeMercure();
73
+ const token = await fake.generateSubscribeToken(['/orders/42']);
74
+ assert.equal(token, 'fake-token');
75
+ });
76
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,84 @@
1
+ import { test } from '@japa/runner';
2
+ import { Mercure } from '../src/mercure.js';
3
+ const config = {
4
+ endpoint: 'http://localhost:3000/.well-known/mercure',
5
+ adminToken: 'test-admin-token',
6
+ jwt: { alg: 'HS256', secret: 'test-secret-long-enough-for-tests-ok' },
7
+ };
8
+ test.group('Mercure', (group) => {
9
+ let originalFetch;
10
+ group.each.setup(() => {
11
+ originalFetch = globalThis.fetch;
12
+ });
13
+ group.each.teardown(() => {
14
+ globalThis.fetch = originalFetch;
15
+ });
16
+ test('generate returns a signed JWT string', async ({ assert }) => {
17
+ const mercure = new Mercure(config);
18
+ const token = await mercure.generate({ subscribe: ['/chat/1'] });
19
+ assert.isString(token);
20
+ assert.equal(token.split('.').length, 3);
21
+ });
22
+ test('generateSubscribeToken returns JWT with subscribe claim', async ({ assert }) => {
23
+ const mercure = new Mercure(config);
24
+ const token = await mercure.generateSubscribeToken(['/orders/42']);
25
+ const raw = token.split('.')[1];
26
+ const payload = JSON.parse(Buffer.from(raw, 'base64url').toString());
27
+ assert.deepEqual(payload.mercure.subscribe, ['/orders/42']);
28
+ });
29
+ test('send publishes topic and data to the hub', async ({ assert }) => {
30
+ let capturedBody = '';
31
+ globalThis.fetch = async (_url, init) => {
32
+ capturedBody = init.body;
33
+ return new Response('1', { status: 200 });
34
+ };
35
+ await new Mercure(config).send('/topic', { msg: 'hello' });
36
+ const body = new URLSearchParams(capturedBody);
37
+ assert.equal(body.get('topic'), '/topic');
38
+ assert.deepEqual(JSON.parse(body.get('data')), { msg: 'hello' });
39
+ });
40
+ test('send converts boolean true to private=on', async ({ assert }) => {
41
+ let capturedBody = '';
42
+ globalThis.fetch = async (_url, init) => {
43
+ capturedBody = init.body;
44
+ return new Response('1', { status: 200 });
45
+ };
46
+ await new Mercure(config).send('/topic', {}, true);
47
+ const body = new URLSearchParams(capturedBody);
48
+ assert.equal(body.get('private'), 'on');
49
+ });
50
+ test('send accepts a SendOptions object', async ({ assert }) => {
51
+ let capturedBody = '';
52
+ globalThis.fetch = async (_url, init) => {
53
+ capturedBody = init.body;
54
+ return new Response('1', { status: 200 });
55
+ };
56
+ await new Mercure(config).send('/topic', {}, { private: true, id: 'evt-1', type: 'order.shipped' });
57
+ const body = new URLSearchParams(capturedBody);
58
+ assert.equal(body.get('private'), 'on');
59
+ assert.equal(body.get('id'), 'evt-1');
60
+ assert.equal(body.get('type'), 'order.shipped');
61
+ });
62
+ test('ping returns true when hub responds', async ({ assert }) => {
63
+ globalThis.fetch = async () => new Response('', { status: 200 });
64
+ assert.isTrue(await new Mercure(config).ping());
65
+ });
66
+ test('ping returns true even on hub 4xx response', async ({ assert }) => {
67
+ globalThis.fetch = async () => new Response('', { status: 401 });
68
+ assert.isTrue(await new Mercure(config).ping());
69
+ });
70
+ test('ping returns false on network error', async ({ assert }) => {
71
+ globalThis.fetch = async () => {
72
+ throw new Error('Network error');
73
+ };
74
+ assert.isFalse(await new Mercure(config).ping());
75
+ });
76
+ test('ping returns false on timeout', async ({ assert }) => {
77
+ globalThis.fetch = async () => {
78
+ const err = new Error('The operation was aborted');
79
+ err.name = 'AbortError';
80
+ throw err;
81
+ };
82
+ assert.isFalse(await new Mercure(config).ping());
83
+ });
84
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,101 @@
1
+ import { test } from '@japa/runner';
2
+ import { Publisher } from '../src/publisher.js';
3
+ import { MercurePublishError } from '../src/errors/publish_error.js';
4
+ import { MercureTimeoutError } from '../src/errors/timeout_error.js';
5
+ const config = {
6
+ endpoint: 'http://localhost:3000/.well-known/mercure',
7
+ adminToken: 'test-token',
8
+ jwt: { alg: 'HS256', secret: 'test-secret' },
9
+ };
10
+ test.group('Publisher', (group) => {
11
+ let originalFetch;
12
+ group.each.setup(() => {
13
+ originalFetch = globalThis.fetch;
14
+ });
15
+ group.each.teardown(() => {
16
+ globalThis.fetch = originalFetch;
17
+ });
18
+ test('posts to the configured endpoint', async ({ assert }) => {
19
+ let capturedUrl = '';
20
+ globalThis.fetch = async (url, _init) => {
21
+ capturedUrl = url;
22
+ return new Response('1', { status: 200 });
23
+ };
24
+ const publisher = new Publisher(config);
25
+ await publisher.publish('test-topic', { hello: 'world' });
26
+ assert.equal(capturedUrl, config.endpoint);
27
+ });
28
+ test('sends topic and data in the body', async ({ assert }) => {
29
+ let capturedBody = '';
30
+ globalThis.fetch = async (_url, init) => {
31
+ capturedBody = init.body;
32
+ return new Response('1', { status: 200 });
33
+ };
34
+ const publisher = new Publisher(config);
35
+ await publisher.publish('my-topic', { msg: 'hello' });
36
+ const body = new URLSearchParams(capturedBody);
37
+ assert.equal(body.get('topic'), 'my-topic');
38
+ assert.deepEqual(JSON.parse(body.get('data')), { msg: 'hello' });
39
+ });
40
+ test('appends private=on when option is set', async ({ assert }) => {
41
+ let capturedBody = '';
42
+ globalThis.fetch = async (_url, init) => {
43
+ capturedBody = init.body;
44
+ return new Response('1', { status: 200 });
45
+ };
46
+ await new Publisher(config).publish('topic', {}, { private: true });
47
+ const body = new URLSearchParams(capturedBody);
48
+ assert.equal(body.get('private'), 'on');
49
+ });
50
+ test('appends id, type, retry when provided', async ({ assert }) => {
51
+ let capturedBody = '';
52
+ globalThis.fetch = async (_url, init) => {
53
+ capturedBody = init.body;
54
+ return new Response('1', { status: 200 });
55
+ };
56
+ await new Publisher(config).publish('topic', {}, { id: 'msg-1', type: 'order.created', retry: 5000 });
57
+ const body = new URLSearchParams(capturedBody);
58
+ assert.equal(body.get('id'), 'msg-1');
59
+ assert.equal(body.get('type'), 'order.created');
60
+ assert.equal(body.get('retry'), '5000');
61
+ });
62
+ test('throws MercurePublishError on non-2xx response', async ({ assert }) => {
63
+ globalThis.fetch = async () => new Response('Unauthorized', { status: 401 });
64
+ let caught;
65
+ try {
66
+ await new Publisher(config).publish('topic', {});
67
+ }
68
+ catch (err) {
69
+ caught = err;
70
+ }
71
+ assert.instanceOf(caught, MercurePublishError);
72
+ assert.equal(caught.statusCode, 401);
73
+ });
74
+ test('throws MercureTimeoutError when request times out', async ({ assert }) => {
75
+ globalThis.fetch = async () => {
76
+ const err = new Error('The operation was aborted');
77
+ err.name = 'AbortError';
78
+ throw err;
79
+ };
80
+ let caught;
81
+ try {
82
+ await new Publisher(config).publish('topic', {});
83
+ }
84
+ catch (err) {
85
+ caught = err;
86
+ }
87
+ assert.instanceOf(caught, MercureTimeoutError);
88
+ assert.equal(caught.timeoutMs, 5000);
89
+ });
90
+ test('handles multiple topics', async ({ assert }) => {
91
+ let capturedBody = '';
92
+ globalThis.fetch = async (_url, init) => {
93
+ capturedBody = init.body;
94
+ return new Response('1', { status: 200 });
95
+ };
96
+ await new Publisher(config).publish(['/a', '/b'], {});
97
+ // URLSearchParams.getAll handles repeated keys
98
+ const body = new URLSearchParams(capturedBody);
99
+ assert.deepEqual(body.getAll('topic'), ['/a', '/b']);
100
+ });
101
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ import { test } from '@japa/runner';
2
+ import { TokenGenerator } from '../src/token_generator.js';
3
+ const config = {
4
+ endpoint: 'http://localhost:3000/.well-known/mercure',
5
+ adminToken: 'test-token',
6
+ jwt: { alg: 'HS256', secret: 'test-secret' },
7
+ };
8
+ test.group('TokenGenerator', () => {
9
+ test('generate returns a JWT string', async ({ assert }) => {
10
+ const generator = new TokenGenerator(config);
11
+ const token = await generator.generate({ subscribe: ['*'] });
12
+ assert.isString(token);
13
+ assert.equal(token.split('.').length, 3);
14
+ });
15
+ test('generated token contains the mercure claim', async ({ assert }) => {
16
+ const generator = new TokenGenerator(config);
17
+ const token = await generator.generate({ subscribe: ['/chat/1'] });
18
+ const raw = token.split('.')[1];
19
+ const payload = JSON.parse(Buffer.from(raw, 'base64url').toString());
20
+ assert.deepEqual(payload.mercure.subscribe, ['/chat/1']);
21
+ });
22
+ test('generateSubscribeToken wraps topics in subscribe field', async ({ assert }) => {
23
+ const generator = new TokenGenerator(config);
24
+ const token = await generator.generateSubscribeToken(['/orders/42']);
25
+ const raw = token.split('.')[1];
26
+ const payload = JSON.parse(Buffer.from(raw, 'base64url').toString());
27
+ assert.deepEqual(payload.mercure.subscribe, ['/orders/42']);
28
+ });
29
+ });
package/configure.ts ADDED
@@ -0,0 +1,41 @@
1
+ /*
2
+ |--------------------------------------------------------------------------
3
+ | Configure hook
4
+ |--------------------------------------------------------------------------
5
+ |
6
+ | The configure hook is called when someone runs "node ace configure <package>"
7
+ | command. You are free to perform any operations inside this function to
8
+ | configure the package.
9
+ |
10
+ | To make things easier, you have access to the underlying "ConfigureCommand"
11
+ | instance and you can use codemods to modify the source files.
12
+ |
13
+ */
14
+
15
+ import ConfigureCommand from '@adonisjs/core/commands/configure'
16
+ import { stubsRoot } from './stubs/main.js'
17
+
18
+ export async function configure(_command: ConfigureCommand) {
19
+ const codemods = await _command.createCodemods()
20
+
21
+ await codemods.makeUsingStub(stubsRoot, 'config/mercure.stub', {})
22
+
23
+ await codemods.defineEnvVariables({
24
+ MERCURE_ENDPOINT: 'http://localhost:3000/.well-known/mercure',
25
+ MERCURE_ADMIN_JWT: '',
26
+ MERCURE_JWT_SECRET: '',
27
+ })
28
+
29
+ await codemods.defineEnvValidations({
30
+ variables: {
31
+ MERCURE_ENDPOINT: `Env.schema.string({ format: 'url' })`,
32
+ MERCURE_ADMIN_JWT: `Env.schema.string()`,
33
+ MERCURE_JWT_SECRET: `Env.schema.string()`,
34
+ },
35
+ leadingComment: 'Mercure configuration',
36
+ })
37
+
38
+ await codemods.updateRcFile((rcFile) => {
39
+ rcFile.addProvider('@mdsiha369/adonis-mercure/mercure_provider')
40
+ })
41
+ }
package/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ /*
2
+ |--------------------------------------------------------------------------
3
+ | Package entrypoint
4
+ |--------------------------------------------------------------------------
5
+ */
6
+ export { configure } from './configure.js'
7
+ export { Mercure } from './src/mercure.js'
8
+ export { FakeMercure } from './src/fake_mercure.js'
9
+ export { defineConfig } from './src/define_config.js'
10
+ export { stubsRoot } from './stubs/main.js'
11
+ export { MercureError } from './src/errors/mercure_error.js'
12
+ export { MercurePublishError } from './src/errors/publish_error.js'
13
+ export { MercureConfigError } from './src/errors/config_error.js'
14
+ export { MercureTimeoutError } from './src/errors/timeout_error.js'
package/package.json ADDED
@@ -0,0 +1,115 @@
1
+ {
2
+ "name": "@mdsiha369/adonis-mercure",
3
+ "description": "Mercure Hub integration for AdonisJS v6 — publish real-time updates via Server-Sent Events (SSE)",
4
+ "version": "2.1.2",
5
+ "engines": {
6
+ "node": ">=20.6.0"
7
+ },
8
+ "main": "./build/index.js",
9
+ "type": "module",
10
+ "files": [
11
+ "src",
12
+ "providers",
13
+ "index.ts",
14
+ "configure.ts",
15
+ "build"
16
+ ],
17
+ "exports": {
18
+ ".": "./build/index.js",
19
+ "./types": "./build/src/types/main.js",
20
+ "./services/main": "./build/services/mercure.js",
21
+ "./mercure_provider": "./build/providers/mercure_provider.js"
22
+ },
23
+ "scripts": {
24
+ "clean": "del-cli build",
25
+ "copy:templates": "copyfiles \"stubs/**/*.stub\" build",
26
+ "typecheck": "tsc --noEmit",
27
+ "lint": "eslint . --ext=.ts",
28
+ "format": "prettier --write .",
29
+ "quick:test": "node --import=./tsnode.esm.js --enable-source-maps bin/test.ts",
30
+ "pretest": "npm run lint",
31
+ "test": "c8 npm run quick:test",
32
+ "prebuild": "npm run lint && npm run clean",
33
+ "build": "tsc",
34
+ "postbuild": "npm run copy:templates",
35
+ "release": "np",
36
+ "version": "npm run build",
37
+ "prepublishOnly": "npm run build"
38
+ },
39
+ "devDependencies": {
40
+ "@adonisjs/assembler": "^7.7.0",
41
+ "@adonisjs/core": "^6.12.1",
42
+ "@adonisjs/eslint-config": "^1.3.0",
43
+ "@adonisjs/prettier-config": "^1.3.0",
44
+ "@adonisjs/tsconfig": "^1.3.0",
45
+ "@japa/assert": "^3.0.0",
46
+ "@japa/runner": "^3.1.4",
47
+ "@swc/core": "^1.7.10",
48
+ "@types/jws": "^3.2.10",
49
+ "@types/node": "^22.2.0",
50
+ "c8": "^10.1.2",
51
+ "copyfiles": "^2.4.1",
52
+ "del-cli": "^5.1.0",
53
+ "eslint": "^8.57.0",
54
+ "np": "^10.0.7",
55
+ "prettier": "^3.3.3",
56
+ "ts-node": "^10.9.2",
57
+ "typescript": "^5.5.4"
58
+ },
59
+ "dependencies": {
60
+ "jws": "^4.0.0"
61
+ },
62
+ "peerDependencies": {
63
+ "@adonisjs/core": "^6.2.0"
64
+ },
65
+ "author": "Michael DAŞ",
66
+ "license": "MIT",
67
+ "homepage": "https://github.com/mdsiha/adonis-mercure#readme",
68
+ "repository": {
69
+ "type": "git",
70
+ "url": "git+ssh://git@github.com/mdsiha/adonis-mercure.git"
71
+ },
72
+ "bugs": {
73
+ "url": "https://github.com/mdsiha/adonis-mercure/issues"
74
+ },
75
+ "keywords": [
76
+ "adonisjs",
77
+ "adonisjs-package",
78
+ "mercure",
79
+ "sse",
80
+ "server-sent-events",
81
+ "realtime",
82
+ "real-time",
83
+ "typescript",
84
+ "jwt",
85
+ "push-notifications",
86
+ "nodejs"
87
+ ],
88
+ "types": "./build/index.d.ts",
89
+ "eslintConfig": {
90
+ "extends": "@adonisjs/eslint-config/package"
91
+ },
92
+ "prettier": "@adonisjs/prettier-config",
93
+ "publishConfig": {
94
+ "access": "public",
95
+ "tag": "latest"
96
+ },
97
+ "np": {
98
+ "message": "chore(release): %s",
99
+ "tag": "latest",
100
+ "branch": "main",
101
+ "anyBranch": false
102
+ },
103
+ "c8": {
104
+ "reporter": [
105
+ "text",
106
+ "html"
107
+ ],
108
+ "exclude": [
109
+ "tests/**"
110
+ ]
111
+ },
112
+ "directories": {
113
+ "test": "tests"
114
+ }
115
+ }
@@ -0,0 +1,23 @@
1
+ import '../src/types/extended.js'
2
+ import { Mercure } from '../src/mercure.js'
3
+ import { MercureConfigError } from '../src/errors/config_error.js'
4
+ import type { ApplicationService } from '@adonisjs/core/types'
5
+ import type { MercureConfig } from '../src/types/main.js'
6
+
7
+ export default class MercureProvider {
8
+ constructor(protected app: ApplicationService) {}
9
+
10
+ async boot() {
11
+ this.app.container.singleton('mercure', async () => {
12
+ const config = this.app.config.get<MercureConfig | undefined>('mercure')
13
+
14
+ if (!config?.endpoint || !config?.adminToken || !config?.jwt?.secret) {
15
+ throw new MercureConfigError(
16
+ 'Missing or incomplete Mercure config. Run "node ace configure @mdsiha369/adonis-mercure"'
17
+ )
18
+ }
19
+
20
+ return new Mercure(config)
21
+ })
22
+ }
23
+ }