@openora/core 0.4.1-canary.21 → 0.4.1-canary.25

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 (122) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/casino/lobby/__tests__/lobby.cache.test.js +1 -1
  3. package/dist/casino/lobby/__tests__/lobby.cache.test.js.map +1 -1
  4. package/dist/cms/__tests__/cms.reads.test.d.ts +2 -0
  5. package/dist/cms/__tests__/cms.reads.test.d.ts.map +1 -0
  6. package/dist/cms/__tests__/{cms.cache.test.js → cms.reads.test.js} +2 -2
  7. package/dist/cms/__tests__/cms.reads.test.js.map +1 -0
  8. package/dist/cms/__tests__/cms.router.test.js +25 -22
  9. package/dist/cms/__tests__/cms.router.test.js.map +1 -1
  10. package/dist/engagement/chat/__tests__/chat.service.test.js +1 -1
  11. package/dist/engagement/chat/__tests__/chat.service.test.js.map +1 -1
  12. package/dist/iam/__tests__/iam.service.test.js +4 -3
  13. package/dist/iam/__tests__/iam.service.test.js.map +1 -1
  14. package/dist/pam/identity/__tests__/identity.rate-limit.test.js +4 -3
  15. package/dist/pam/identity/__tests__/identity.rate-limit.test.js.map +1 -1
  16. package/dist/pam/identity/__tests__/identity.service.test.js +6 -1
  17. package/dist/pam/identity/__tests__/identity.service.test.js.map +1 -1
  18. package/dist/pam/identity/service/identity.service.d.ts.map +1 -1
  19. package/dist/pam/identity/service/identity.service.js +1 -0
  20. package/dist/pam/identity/service/identity.service.js.map +1 -1
  21. package/dist/server/kernel/__tests__/cache.test.js +11 -57
  22. package/dist/server/kernel/__tests__/cache.test.js.map +1 -1
  23. package/dist/server/kernel/__tests__/event-bus.test.js +14 -1
  24. package/dist/server/kernel/__tests__/event-bus.test.js.map +1 -1
  25. package/dist/server/kernel/__tests__/rate-limiter.test.js +3 -45
  26. package/dist/server/kernel/__tests__/rate-limiter.test.js.map +1 -1
  27. package/dist/server/kernel/__tests__/redis-streams-broker.test.d.ts +2 -0
  28. package/dist/server/kernel/__tests__/redis-streams-broker.test.d.ts.map +1 -0
  29. package/dist/server/kernel/__tests__/redis-streams-broker.test.js +293 -0
  30. package/dist/server/kernel/__tests__/redis-streams-broker.test.js.map +1 -0
  31. package/dist/server/kernel/cache.d.ts +0 -12
  32. package/dist/server/kernel/cache.d.ts.map +1 -1
  33. package/dist/server/kernel/cache.js +0 -55
  34. package/dist/server/kernel/cache.js.map +1 -1
  35. package/dist/server/kernel/event-bus.d.ts +1 -16
  36. package/dist/server/kernel/event-bus.d.ts.map +1 -1
  37. package/dist/server/kernel/event-bus.js +3 -32
  38. package/dist/server/kernel/event-bus.js.map +1 -1
  39. package/dist/server/kernel/index.d.ts +4 -6
  40. package/dist/server/kernel/index.d.ts.map +1 -1
  41. package/dist/server/kernel/index.js +4 -6
  42. package/dist/server/kernel/index.js.map +1 -1
  43. package/dist/server/kernel/rate-limiter.d.ts +1 -10
  44. package/dist/server/kernel/rate-limiter.d.ts.map +1 -1
  45. package/dist/server/kernel/rate-limiter.js +0 -38
  46. package/dist/server/kernel/rate-limiter.js.map +1 -1
  47. package/dist/server/kernel/redis-streams-broker.d.ts +65 -0
  48. package/dist/server/kernel/redis-streams-broker.d.ts.map +1 -0
  49. package/dist/server/kernel/redis-streams-broker.js +208 -0
  50. package/dist/server/kernel/redis-streams-broker.js.map +1 -0
  51. package/dist/server/runtime/__tests__/create-app.test.d.ts +2 -0
  52. package/dist/server/runtime/__tests__/create-app.test.d.ts.map +1 -0
  53. package/dist/server/runtime/__tests__/create-app.test.js +60 -0
  54. package/dist/server/runtime/__tests__/create-app.test.js.map +1 -0
  55. package/dist/server/runtime/assert-durable-seams.d.ts +18 -0
  56. package/dist/server/runtime/assert-durable-seams.d.ts.map +1 -0
  57. package/dist/server/runtime/assert-durable-seams.js +32 -0
  58. package/dist/server/runtime/assert-durable-seams.js.map +1 -0
  59. package/dist/server/runtime/create-app.d.ts +15 -9
  60. package/dist/server/runtime/create-app.d.ts.map +1 -1
  61. package/dist/server/runtime/create-app.js +61 -49
  62. package/dist/server/runtime/create-app.js.map +1 -1
  63. package/dist/testing/fakes/__tests__/cache.test.d.ts +2 -0
  64. package/dist/testing/fakes/__tests__/cache.test.d.ts.map +1 -0
  65. package/dist/testing/fakes/__tests__/cache.test.js +89 -0
  66. package/dist/testing/fakes/__tests__/cache.test.js.map +1 -0
  67. package/dist/testing/fakes/__tests__/job-queue.test.d.ts.map +1 -0
  68. package/dist/{server/kernel → testing/fakes}/__tests__/job-queue.test.js +1 -1
  69. package/dist/testing/fakes/__tests__/job-queue.test.js.map +1 -0
  70. package/dist/testing/fakes/__tests__/rate-limiter.test.d.ts +2 -0
  71. package/dist/testing/fakes/__tests__/rate-limiter.test.d.ts.map +1 -0
  72. package/dist/testing/fakes/__tests__/rate-limiter.test.js +83 -0
  73. package/dist/testing/fakes/__tests__/rate-limiter.test.js.map +1 -0
  74. package/dist/testing/fakes/__tests__/realtime-authorizer.test.d.ts.map +1 -0
  75. package/dist/{server/kernel → testing/fakes}/__tests__/realtime-authorizer.test.js.map +1 -1
  76. package/dist/testing/fakes/__tests__/realtime-transport.test.d.ts.map +1 -0
  77. package/dist/{server/kernel → testing/fakes}/__tests__/realtime-transport.test.js.map +1 -1
  78. package/dist/testing/fakes/cache.d.ts +14 -0
  79. package/dist/testing/fakes/cache.d.ts.map +1 -0
  80. package/dist/testing/fakes/cache.js +56 -0
  81. package/dist/testing/fakes/cache.js.map +1 -0
  82. package/dist/testing/fakes/event-bus.d.ts +17 -0
  83. package/dist/testing/fakes/event-bus.d.ts.map +1 -0
  84. package/dist/testing/fakes/event-bus.js +30 -0
  85. package/dist/testing/fakes/event-bus.js.map +1 -0
  86. package/dist/{server/kernel → testing/fakes}/job-queue.d.ts +9 -9
  87. package/dist/{server/kernel → testing/fakes}/job-queue.d.ts.map +1 -1
  88. package/dist/{server/kernel → testing/fakes}/job-queue.js +9 -9
  89. package/dist/{server/kernel → testing/fakes}/job-queue.js.map +1 -1
  90. package/dist/testing/fakes/rate-limiter.d.ts +11 -0
  91. package/dist/testing/fakes/rate-limiter.d.ts.map +1 -0
  92. package/dist/testing/fakes/rate-limiter.js +39 -0
  93. package/dist/testing/fakes/rate-limiter.js.map +1 -0
  94. package/dist/{server/kernel → testing/fakes}/realtime-authorizer.d.ts.map +1 -1
  95. package/dist/{server/kernel → testing/fakes}/realtime-authorizer.js.map +1 -1
  96. package/dist/{server/kernel → testing/fakes}/realtime-transport.d.ts.map +1 -1
  97. package/dist/{server/kernel → testing/fakes}/realtime-transport.js +3 -1
  98. package/dist/testing/fakes/realtime-transport.js.map +1 -0
  99. package/dist/testing/index.d.ts +7 -0
  100. package/dist/testing/index.d.ts.map +1 -0
  101. package/dist/testing/index.js +11 -0
  102. package/dist/testing/index.js.map +1 -0
  103. package/dist/wallet/__tests__/wallet.rate-limit.test.js +1 -1
  104. package/dist/wallet/__tests__/wallet.rate-limit.test.js.map +1 -1
  105. package/package.json +6 -1
  106. package/turbo-generators/src/templates/module/agents.hbs +16 -57
  107. package/dist/cms/__tests__/cms.cache.test.d.ts +0 -2
  108. package/dist/cms/__tests__/cms.cache.test.d.ts.map +0 -1
  109. package/dist/cms/__tests__/cms.cache.test.js.map +0 -1
  110. package/dist/server/kernel/__tests__/job-queue.test.d.ts.map +0 -1
  111. package/dist/server/kernel/__tests__/job-queue.test.js.map +0 -1
  112. package/dist/server/kernel/__tests__/realtime-authorizer.test.d.ts.map +0 -1
  113. package/dist/server/kernel/__tests__/realtime-transport.test.d.ts.map +0 -1
  114. package/dist/server/kernel/realtime-transport.js.map +0 -1
  115. /package/dist/{server/kernel → testing/fakes}/__tests__/job-queue.test.d.ts +0 -0
  116. /package/dist/{server/kernel → testing/fakes}/__tests__/realtime-authorizer.test.d.ts +0 -0
  117. /package/dist/{server/kernel → testing/fakes}/__tests__/realtime-authorizer.test.js +0 -0
  118. /package/dist/{server/kernel → testing/fakes}/__tests__/realtime-transport.test.d.ts +0 -0
  119. /package/dist/{server/kernel → testing/fakes}/__tests__/realtime-transport.test.js +0 -0
  120. /package/dist/{server/kernel → testing/fakes}/realtime-authorizer.d.ts +0 -0
  121. /package/dist/{server/kernel → testing/fakes}/realtime-authorizer.js +0 -0
  122. /package/dist/{server/kernel → testing/fakes}/realtime-transport.d.ts +0 -0
@@ -0,0 +1,208 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { createLogger } from './logger.js';
3
+ const STREAM_PREFIX = 'oss:evt:';
4
+ const STREAM_MAXLEN = 10_000;
5
+ const READ_COUNT = 10;
6
+ const BLOCK_MS = 5_000;
7
+ const CLAIM_INTERVAL_MS = 30_000;
8
+ const CLAIM_MIN_IDLE_MS = 60_000;
9
+ const RETRY_DELAY_MS = 2_000;
10
+ function delay(ms) {
11
+ return new Promise((resolve) => setTimeout(resolve, ms));
12
+ }
13
+ function streamKey(topic) {
14
+ return STREAM_PREFIX + topic;
15
+ }
16
+ function isBusyGroupError(err) {
17
+ return err instanceof Error && err.message.includes('BUSYGROUP');
18
+ }
19
+ /**
20
+ * Durable `MESSAGE_BROKER` reference driver on Redis Streams, auto-bound by
21
+ * `createApp` when `REDIS_URL` is set (ADR-0030 - production is distributed-only,
22
+ * core ships no in-process default).
23
+ *
24
+ * Every `subscribe(topic, handler)` call for the same (topic, group) - default
25
+ * group is the deployment's `serviceName` - coalesces into ONE Redis consumer-group
26
+ * loop that fans out to every registered local handler, then XACKs once: an event
27
+ * is delivered to exactly one replica of the service (competing consumers across
28
+ * replicas via the shared group), and that replica runs every local handler for
29
+ * the topic (matches in-process fan-out semantics, but once per cluster, not once
30
+ * per replica). A distinct `SERVICE_NAME` per split service gives each service its
31
+ * own copy of every event; `createApp` requires one as soon as `SERVICE_MANIFEST`
32
+ * is set, since the group name is a durable identity and can't be derived from a
33
+ * module list that reorders and grows.
34
+ *
35
+ * Blocking `XREADGROUP` runs on a `client.duplicate()`d connection per loop, never
36
+ * on the shared client - `publish` (`XADD`, non-blocking) uses the shared client,
37
+ * so a blocked read never stalls the cache/rate-limiter commands sharing it.
38
+ * `publish` awaits the XADD and lets errors propagate (the `OutboxRelay` marks a
39
+ * row published only after this resolves - it must be reliable, not a silent
40
+ * no-op). Handler errors are guarded + logged, then the entry is XACKed regardless
41
+ * (matches `EventBus.on` - no per-handler retry; must-not-lose events use the
42
+ * outbox). A crash before XACK leaves the entry pending; `XAUTOCLAIM` periodically
43
+ * reclaims idle-pending entries so at-least-once delivery survives a replica crash.
44
+ *
45
+ * At-least-once holds from group creation onward, NOT before it. A group is created
46
+ * at `startId` (default `$`) on first subscribe and persists in Redis from then on,
47
+ * so restarts and outages are covered: the group keeps its cursor, undelivered
48
+ * entries wait in the stream, and pending ones are reclaimed. The one uncovered
49
+ * window is a group's FIRST-EVER creation - a service extracted after events were
50
+ * already flowing starts from `$` and never sees the backlog. `startId: '0'` closes
51
+ * that window by replaying everything still retained (up to STREAM_MAXLEN), at the
52
+ * cost of re-running every handler for those events: safe on a fresh deploy (empty
53
+ * streams), a duplicate-side-effect hazard for a late-added service, since core has
54
+ * no dedup layer - `EventBus.on` invokes handlers directly, and `eventId` is a key
55
+ * consumers must dedup on themselves.
56
+ *
57
+ * `orderingKey` is NOT honoured - Redis Streams have no partitioning, so per-key
58
+ * ordering across concurrent consumers isn't guaranteed (same footnote as
59
+ * `BullMqJobQueue`'s `orderingKey`).
60
+ */
61
+ export class RedisStreamsBroker {
62
+ client;
63
+ logger = createLogger('message-broker');
64
+ serviceName;
65
+ startId;
66
+ consumerId = randomUUID();
67
+ loops = new Map();
68
+ constructor(client, opts) {
69
+ this.client = client;
70
+ this.serviceName = opts.serviceName;
71
+ this.startId = opts.startId ?? '$';
72
+ }
73
+ async publish(envelope) {
74
+ await this.client.xAdd(streamKey(envelope.topic), '*', { data: JSON.stringify(envelope) }, { TRIM: { strategy: 'MAXLEN', strategyModifier: '~', threshold: STREAM_MAXLEN } });
75
+ }
76
+ subscribe(topic, handler, options) {
77
+ const group = options?.consumerGroup ?? this.serviceName;
78
+ const key = streamKey(topic);
79
+ const loopKey = `${key}::${group}`;
80
+ let loop = this.loops.get(loopKey);
81
+ if (!loop) {
82
+ loop = this.startLoop(key, group);
83
+ this.loops.set(loopKey, loop);
84
+ }
85
+ loop.handlers.add(handler);
86
+ return () => {
87
+ const current = this.loops.get(loopKey);
88
+ if (!current || !current.handlers.delete(handler)) {
89
+ return;
90
+ }
91
+ if (current.handlers.size === 0) {
92
+ this.loops.delete(loopKey);
93
+ current.stop();
94
+ }
95
+ };
96
+ }
97
+ async close() {
98
+ const loops = [...this.loops.values()];
99
+ this.loops.clear();
100
+ for (const loop of loops) {
101
+ loop.stop();
102
+ }
103
+ await Promise.allSettled(loops.map((loop) => loop.done));
104
+ }
105
+ startLoop(key, group) {
106
+ const handlers = new Set();
107
+ const reader = this.client.duplicate();
108
+ reader.on('error', (err) => this.logger.error({ err, key, group }, 'stream reader connection error'));
109
+ let stopped = false;
110
+ let lastClaimAt = 0;
111
+ const stop = () => {
112
+ stopped = true;
113
+ // Rejects the in-flight blocking XREADGROUP (if any) immediately, instead of
114
+ // waiting up to BLOCK_MS for it to time out on its own.
115
+ reader.destroy();
116
+ };
117
+ // `ready` gates connecting + group creation, and stays false until BOTH have
118
+ // succeeded - so a Redis outage at loop start retries the whole handshake with
119
+ // backoff rather than permanently abandoning this (topic, group)'s consumption.
120
+ // It is NOT reset on a later error: node-redis owns reconnection from there on
121
+ // (socket.reconnectStrategy re-establishes the connection and re-queues the
122
+ // blocked XREADGROUP), and the group already exists, so there is nothing to redo.
123
+ // Both steps are idempotent regardless - `connect()` throws 'Socket already
124
+ // opened' once the socket is open, hence the isOpen guard, and a re-created group
125
+ // comes back BUSYGROUP, which is tolerated below.
126
+ const done = (async () => {
127
+ let ready = false;
128
+ while (!stopped) {
129
+ try {
130
+ if (!ready) {
131
+ if (!reader.isOpen) {
132
+ await reader.connect();
133
+ }
134
+ try {
135
+ await reader.xGroupCreate(key, group, this.startId, { MKSTREAM: true });
136
+ }
137
+ catch (err) {
138
+ if (!isBusyGroupError(err)) {
139
+ throw err;
140
+ }
141
+ }
142
+ ready = true;
143
+ }
144
+ if (Date.now() - lastClaimAt > CLAIM_INTERVAL_MS) {
145
+ lastClaimAt = Date.now();
146
+ await this.reclaimPending(reader, key, group, handlers);
147
+ }
148
+ const reply = (await reader.xReadGroup(group, this.consumerId, { key, id: '>' }, { COUNT: READ_COUNT, BLOCK: BLOCK_MS }));
149
+ if (!reply) {
150
+ continue;
151
+ }
152
+ for (const stream of reply) {
153
+ for (const message of stream.messages) {
154
+ await this.dispatch(reader, key, group, message, handlers);
155
+ }
156
+ }
157
+ }
158
+ catch (err) {
159
+ if (stopped) {
160
+ break;
161
+ }
162
+ this.logger.error({ err, key, group }, 'stream loop error, retrying');
163
+ await delay(RETRY_DELAY_MS);
164
+ }
165
+ }
166
+ })();
167
+ return { handlers, stop, done };
168
+ }
169
+ async reclaimPending(reader, key, group, handlers) {
170
+ try {
171
+ const claimed = (await reader.xAutoClaim(key, group, this.consumerId, CLAIM_MIN_IDLE_MS, '0-0', { COUNT: READ_COUNT }));
172
+ for (const message of claimed.messages) {
173
+ if (message) {
174
+ await this.dispatch(reader, key, group, message, handlers);
175
+ }
176
+ }
177
+ }
178
+ catch (err) {
179
+ this.logger.error({ err, key, group }, 'stream reclaim failed');
180
+ }
181
+ }
182
+ async dispatch(reader, key, group, message, handlers) {
183
+ const raw = message.message['data'];
184
+ if (raw === undefined) {
185
+ await reader.xAck(key, group, message.id);
186
+ return;
187
+ }
188
+ let envelope;
189
+ try {
190
+ envelope = JSON.parse(raw);
191
+ }
192
+ catch (err) {
193
+ this.logger.error({ err, key, group, id: message.id }, 'stream envelope parse failed');
194
+ await reader.xAck(key, group, message.id);
195
+ return;
196
+ }
197
+ for (const fn of handlers) {
198
+ try {
199
+ await fn(envelope);
200
+ }
201
+ catch (err) {
202
+ this.logger.error({ err, topic: envelope.topic }, 'message broker handler threw');
203
+ }
204
+ }
205
+ await reader.xAck(key, group, message.id);
206
+ }
207
+ }
208
+ //# sourceMappingURL=redis-streams-broker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-streams-broker.js","sourceRoot":"","sources":["../../../src/server/kernel/redis-streams-broker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=create-app.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-app.test.d.ts","sourceRoot":"","sources":["../../../../src/server/runtime/__tests__/create-app.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { MESSAGE_BROKER, JOB_QUEUE, CACHE, RATE_LIMITER } from '@openora/core/contracts';
3
+ import { InMemoryBroker, InProcessJobQueue, InProcessCache, InProcessRateLimiter, } from '@openora/core/testing';
4
+ import { createLogger } from '../../kernel/index.js';
5
+ import { createApp } from '../create-app.js';
6
+ // A syntactically valid but unreachable DB url - fine here because nothing in this
7
+ // suite ever runs a query: DrizzleService's pg.Pool connects lazily, and neither
8
+ // health.ping nor better-auth's getSession() touch the DB when the request carries
9
+ // no session cookie (getSessionFromCtx short-circuits on a missing cookie).
10
+ const DUMMY_DATABASE_URL = 'postgres://test:test@127.0.0.1:1/create_app_test';
11
+ describe('createApp - distributed-only durable seams (ADR-0030)', () => {
12
+ it('throws a clear, actionable error when no durable seam is bound', async () => {
13
+ await expect(createApp({ plugins: [], databaseUrl: DUMMY_DATABASE_URL, openapi: { enabled: false } })).rejects.toThrow(/MESSAGE_BROKER.*JOB_QUEUE.*CACHE.*RATE_LIMITER/s);
14
+ });
15
+ it('boots and serves once the fakes are bound via configure', async () => {
16
+ const created = await createApp({
17
+ plugins: [],
18
+ databaseUrl: DUMMY_DATABASE_URL,
19
+ openapi: { enabled: false },
20
+ configure(container) {
21
+ container.register(MESSAGE_BROKER, () => new InMemoryBroker());
22
+ container.register(JOB_QUEUE, () => new InProcessJobQueue(createLogger('job-queue')));
23
+ container.register(CACHE, () => new InProcessCache());
24
+ container.register(RATE_LIMITER, () => new InProcessRateLimiter());
25
+ },
26
+ });
27
+ const res = await created.app.request('/health');
28
+ expect(res.status).toBe(200);
29
+ expect(await res.json()).toMatchObject({ status: 'ok' });
30
+ await created.close();
31
+ });
32
+ });
33
+ describe('createApp - service name for the Redis Streams consumer group', () => {
34
+ const saved = { redis: process.env['REDIS_URL'], manifest: process.env['SERVICE_MANIFEST'] };
35
+ afterEach(() => {
36
+ // Restore rather than delete: a bare delete would strip a REDIS_URL the rest of
37
+ // the suite (or a CI service container) legitimately set.
38
+ for (const [key, value] of [
39
+ ['REDIS_URL', saved.redis],
40
+ ['SERVICE_MANIFEST', saved.manifest],
41
+ ]) {
42
+ if (value === undefined) {
43
+ delete process.env[key];
44
+ }
45
+ else {
46
+ process.env[key] = value;
47
+ }
48
+ }
49
+ });
50
+ it('refuses to boot a split service that has no SERVICE_NAME of its own', async () => {
51
+ // The consumer group is a durable identity, so it can never be derived from
52
+ // SERVICE_MANIFEST - that is a list of module ids ('wallet,iam') which reorders
53
+ // and grows. Throws before any Redis client is opened.
54
+ process.env['REDIS_URL'] = 'redis://127.0.0.1:1';
55
+ process.env['SERVICE_MANIFEST'] = 'wallet,iam';
56
+ delete process.env['SERVICE_NAME'];
57
+ await expect(createApp({ plugins: [], databaseUrl: DUMMY_DATABASE_URL, openapi: { enabled: false } })).rejects.toThrow(/SERVICE_MANIFEST is set but SERVICE_NAME is not/);
58
+ });
59
+ });
60
+ //# sourceMappingURL=create-app.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-app.test.js","sourceRoot":"","sources":["../../../../src/server/runtime/__tests__/create-app.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import { type AnyToken } from '@openora/core/contracts';
2
+ /**
3
+ * Production is distributed-only (ADR-0030, superseding ADR-0010/0014/0016/0028):
4
+ * the in-process seam impls no longer ship on the production path, so every
5
+ * always-needed seam must resolve to a durable binding before `createApp` finishes
6
+ * booting. Call this AFTER `loadPlugins` + `configure` so a `REDIS_URL` auto-bind or
7
+ * an overlay's own registration has already won (Container is last-registration-wins).
8
+ * Modeled on `assertSealedServicesBound` (`compliance/assert.ts`) - same `has(token)`
9
+ * shape, same "list every gap, don't stop at the first" style.
10
+ *
11
+ * `REALTIME_TRANSPORT` is deliberately excluded: it stays lazy and throws on first
12
+ * use, since not every deployment serves realtime traffic.
13
+ */
14
+ export type DurableSeamContainerView = {
15
+ has(token: AnyToken<unknown>): boolean;
16
+ };
17
+ export declare function assertDurableSeamsBound(container: DurableSeamContainerView): void;
18
+ //# sourceMappingURL=assert-durable-seams.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assert-durable-seams.d.ts","sourceRoot":"","sources":["../../../src/server/runtime/assert-durable-seams.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,QAAQ,EACd,MAAM,yBAAyB,CAAC;AAEjC;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;CACxC,CAAC;AA2BF,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,wBAAwB,GAAG,IAAI,CASjF"}
@@ -0,0 +1,32 @@
1
+ import { MESSAGE_BROKER, JOB_QUEUE, CACHE, RATE_LIMITER, } from '@openora/core/contracts';
2
+ const REQUIRED_DURABLE_SEAMS = [
3
+ {
4
+ token: MESSAGE_BROKER,
5
+ name: 'MESSAGE_BROKER',
6
+ fix: 'set REDIS_URL to auto-bind RedisStreamsBroker, or bind a MessageBrokerAdapter overlay (eg RabbitMQ/Kafka) via ctx.provide(MESSAGE_BROKER, ...)',
7
+ },
8
+ {
9
+ token: JOB_QUEUE,
10
+ name: 'JOB_QUEUE',
11
+ fix: 'set REDIS_URL to auto-bind BullMqJobQueue, or bind a JOB_QUEUE overlay directly',
12
+ },
13
+ {
14
+ token: CACHE,
15
+ name: 'CACHE',
16
+ fix: 'set REDIS_URL to auto-bind RedisCache, or bind a CACHE overlay directly',
17
+ },
18
+ {
19
+ token: RATE_LIMITER,
20
+ name: 'RATE_LIMITER',
21
+ fix: 'set REDIS_URL to auto-bind RedisRateLimiter, or bind a RATE_LIMITER overlay directly',
22
+ },
23
+ ];
24
+ export function assertDurableSeamsBound(container) {
25
+ const missing = REQUIRED_DURABLE_SEAMS.filter((seam) => !container.has(seam.token));
26
+ if (missing.length === 0) {
27
+ return;
28
+ }
29
+ throw new Error(`[create-app] production is distributed-only - the following seams have no durable binding:\n` +
30
+ missing.map((seam) => ` - ${seam.name}: ${seam.fix}`).join('\n'));
31
+ }
32
+ //# sourceMappingURL=assert-durable-seams.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assert-durable-seams.js","sourceRoot":"","sources":["../../../src/server/runtime/assert-durable-seams.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,SAAS,EACT,KAAK,EACL,YAAY,GAEb,MAAM,yBAAyB,CAAC;AAoBjC,MAAM,sBAAsB,GAA+B;IACzD;QACE,KAAK,EAAE,cAAc;QACrB,IAAI,EAAE,gBAAgB;QACtB,GAAG,EAAE,gJAAgJ;KACtJ;IACD;QACE,KAAK,EAAE,SAAS;QAChB,IAAI,EAAE,WAAW;QACjB,GAAG,EAAE,iFAAiF;KACvF;IACD;QACE,KAAK,EAAE,KAAK;QACZ,IAAI,EAAE,OAAO;QACb,GAAG,EAAE,yEAAyE;KAC/E;IACD;QACE,KAAK,EAAE,YAAY;QACnB,IAAI,EAAE,cAAc;QACpB,GAAG,EAAE,sFAAsF;KAC5F;CACF,CAAC;AAEF,MAAM,UAAU,uBAAuB,CAAC,SAAmC;IACzE,MAAM,OAAO,GAAG,sBAAsB,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IACpF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO;IACT,CAAC;IACD,MAAM,IAAI,KAAK,CACb,8FAA8F;QAC5F,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CACpE,CAAC;AACJ,CAAC"}
@@ -38,15 +38,21 @@ export type CreatedApp = {
38
38
  };
39
39
  /**
40
40
  * The single composition root: wires the DI container, boots every plugin in
41
- * `config.plugins`, and mounts the resulting oRPC router on a Hono app. Seam
42
- * defaults auto-upgrade when env vars are set - `REDIS_URL` binds `JOB_QUEUE`/
43
- * `RATE_LIMITER`/`CACHE` to their Redis-backed drivers, `OUTBOX_ENABLED` (or
44
- * `AMQP_URL`/`RABBITMQ_URL`) enables the transactional outbox relay - otherwise
45
- * every seam stays in-process (zero deps for dev/test). `container.dispose()`
46
- * (via `close()`) runs disposers in REVERSE registration order, so DRIZZLE is
47
- * registered before JOB_QUEUE to guarantee workers drain before the DB closes.
48
- * A router namespace registered by more than one plugin throws at boot, not at
49
- * request time.
41
+ * `config.plugins`, and mounts the resulting oRPC router on a Hono app.
42
+ * Production is distributed-only (ADR-0030, superseding ADR-0010/0014/0016/0028):
43
+ * `REDIS_URL` auto-binds `JOB_QUEUE`/`RATE_LIMITER`/`CACHE`/`MESSAGE_BROKER` to their
44
+ * Redis-backed drivers - the broker being the `RedisStreamsBroker` reference driver,
45
+ * which an overlay (RabbitMQ/Kafka) or a test's `configure` callback can replace.
46
+ * `assertDurableSeamsBound` runs right after
47
+ * plugins + `configure` and throws a clear, actionable error listing every
48
+ * always-needed seam that still has no binding - it never falls back to an
49
+ * in-process impl. `REALTIME_TRANSPORT` stays lazy (throws on first use) since not
50
+ * every deployment serves realtime traffic. `OUTBOX_ENABLED` (or
51
+ * `AMQP_URL`/`RABBITMQ_URL`) enables the transactional outbox relay.
52
+ * `container.dispose()` (via `close()`) runs disposers in REVERSE registration
53
+ * order, so DRIZZLE is registered before JOB_QUEUE to guarantee workers drain
54
+ * before the DB closes. A router namespace registered by more than one plugin
55
+ * throws at boot, not at request time.
50
56
  */
51
57
  export declare function createApp(config: CreateAppConfig): Promise<CreatedApp>;
52
58
  //# sourceMappingURL=create-app.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"create-app.d.ts","sourceRoot":"","sources":["../../../src/server/runtime/create-app.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAO5B,OAAO,EACL,SAAS,EAiBV,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAaL,KAAK,aAAa,EAEnB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAsBxE,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,WAAW,EAAE,CAAC;IAOvB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAErC,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,IAAI,CAAC,EAAE,OAAO,GAAG;QAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;KAAE,CAAC;IAEjD,WAAW,CAAC,EAAE,MAAM,CAAC;IAKrB,QAAQ,CAAC,EAAE,cAAc,CAAC,GAAG,CAAC,CAAC;IAE/B,OAAO,CAAC,EAAE;QACR,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,IAAI,CAAC,EAAE;YAAE,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IAEF,OAAO,CAAC,EAAE,aAAa,CAAC;IAIxB,SAAS,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,KAAK,CAAC;IAEjE,SAAS,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3D,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,GAAG,EAAE,IAAI,CAAC;IACV,SAAS,EAAE,SAAS,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,eAAe,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB,CAAC;AAqDF;;;;;;;;;;;GAWG;AACH,wBAAsB,SAAS,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CA6R5E"}
1
+ {"version":3,"file":"create-app.d.ts","sourceRoot":"","sources":["../../../src/server/runtime/create-app.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAO5B,OAAO,EACL,SAAS,EAYV,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAWL,KAAK,aAAa,EAEnB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAC;AA8CxE,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,WAAW,EAAE,CAAC;IAOvB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAErC,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,IAAI,CAAC,EAAE,OAAO,GAAG;QAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;KAAE,CAAC;IAEjD,WAAW,CAAC,EAAE,MAAM,CAAC;IAKrB,QAAQ,CAAC,EAAE,cAAc,CAAC,GAAG,CAAC,CAAC;IAE/B,OAAO,CAAC,EAAE;QACR,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,IAAI,CAAC,EAAE;YAAE,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IAEF,OAAO,CAAC,EAAE,aAAa,CAAC;IAIxB,SAAS,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,KAAK,CAAC;IAEjE,SAAS,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3D,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,GAAG,EAAE,IAAI,CAAC;IACV,SAAS,EAAE,SAAS,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,eAAe,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB,CAAC;AAqDF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,SAAS,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CAgR5E"}
@@ -8,12 +8,13 @@ import { HTTPException } from 'hono/http-exception';
8
8
  import { serve } from '@hono/node-server';
9
9
  import { resolve } from 'node:path';
10
10
  import { generateOpenApiSpec } from './openapi.js';
11
- import { Container, BullMqJobQueue, InMemoryBroker, InProcessCache, InProcessJobQueue, InProcessRateLimiter, InProcessRealtimeTransport, RedisCache, RedisRateLimiter, SseClientAuthorizer, createEventBus, createLogger, createRedisClient, withRequestContext, setErrorReporter, EVENT_BUS, } from '../kernel/index.js';
11
+ import { Container, BullMqJobQueue, RedisCache, RedisRateLimiter, RedisStreamsBroker, createEventBus, createLogger, createRedisClient, withRequestContext, setErrorReporter, EVENT_BUS, } from '../kernel/index.js';
12
12
  import { randomUUID } from 'node:crypto';
13
- import { MESSAGE_BROKER, JOB_QUEUE, REALTIME_TRANSPORT, REALTIME_CLIENT_AUTHORIZER, OUTBOX, ADMIN_PERMISSION_RESOLVER, RATE_LIMITER, CACHE, ERROR_TRACKING, composeContract, healthContract, IGAMING_CONFIG, PLATFORM_CONFIG, } from '@openora/core/contracts';
13
+ import { MESSAGE_BROKER, JOB_QUEUE, OUTBOX, ADMIN_PERMISSION_RESOLVER, RATE_LIMITER, CACHE, ERROR_TRACKING, composeContract, healthContract, IGAMING_CONFIG, PLATFORM_CONFIG, } from '@openora/core/contracts';
14
14
  import { DrizzleService, DRIZZLE, DrizzleOutboxWriter, OutboxRelay } from '../db/index.js';
15
15
  import { AdminGuard, ADMIN_GUARD, SessionResolver, AUTH_SESSION } from '../auth/index.js';
16
16
  import { loadPlugins } from '../plugin-host/index.js';
17
+ import { assertDurableSeamsBound } from './assert-durable-seams.js';
17
18
  import { loadPlatformConfig, resolvePlatformConfigPath } from '../kernel/platform-config-loader.js';
18
19
  // Path prefixes safe to cache at the HTTP layer: public, non-personalized reads
19
20
  // only (lobby feeds, public CMS content, the game catalogue). NOTHING
@@ -32,6 +33,26 @@ const PUBLIC_HTTP_CACHE_PATHS = [
32
33
  ];
33
34
  const DEFAULT_HTTP_CACHE_MAX_AGE_SECONDS = 30;
34
35
  const DEFAULT_HTTP_CACHE_STALE_WHILE_REVALIDATE_SECONDS = 60;
36
+ // Redis Streams consumer group name for MESSAGE_BROKER. Distinct deployments/services
37
+ // sharing one Redis MUST use distinct names so their subscribers don't compete for
38
+ // each other's events. The name is a durable identity: renaming it strands the old
39
+ // group's pending (delivered-but-unacked) entries and starts the new one at `$`, so
40
+ // it can never be derived from SERVICE_MANIFEST - that is a LIST of module ids
41
+ // (see service-manifest.ts) whose value reorders and grows as modules move.
42
+ // A split deployment must therefore name itself explicitly.
43
+ function resolveServiceName() {
44
+ const serviceName = process.env['SERVICE_NAME'];
45
+ if (serviceName) {
46
+ return serviceName;
47
+ }
48
+ if (process.env['SERVICE_MANIFEST']) {
49
+ throw new Error('[create-app] SERVICE_MANIFEST is set but SERVICE_NAME is not. A split service needs a stable ' +
50
+ 'name of its own: it becomes the MESSAGE_BROKER consumer group, and every service sharing a ' +
51
+ 'group competes for the same events instead of each receiving a copy. Set SERVICE_NAME to a ' +
52
+ 'stable identifier for this deployment (eg SERVICE_NAME=wallet).');
53
+ }
54
+ return 'monolith';
55
+ }
35
56
  function headersToRecord(headers) {
36
57
  const out = {};
37
58
  headers.forEach((value, key) => {
@@ -83,15 +104,21 @@ async function captureRawBody(req) {
83
104
  }
84
105
  /**
85
106
  * The single composition root: wires the DI container, boots every plugin in
86
- * `config.plugins`, and mounts the resulting oRPC router on a Hono app. Seam
87
- * defaults auto-upgrade when env vars are set - `REDIS_URL` binds `JOB_QUEUE`/
88
- * `RATE_LIMITER`/`CACHE` to their Redis-backed drivers, `OUTBOX_ENABLED` (or
89
- * `AMQP_URL`/`RABBITMQ_URL`) enables the transactional outbox relay - otherwise
90
- * every seam stays in-process (zero deps for dev/test). `container.dispose()`
91
- * (via `close()`) runs disposers in REVERSE registration order, so DRIZZLE is
92
- * registered before JOB_QUEUE to guarantee workers drain before the DB closes.
93
- * A router namespace registered by more than one plugin throws at boot, not at
94
- * request time.
107
+ * `config.plugins`, and mounts the resulting oRPC router on a Hono app.
108
+ * Production is distributed-only (ADR-0030, superseding ADR-0010/0014/0016/0028):
109
+ * `REDIS_URL` auto-binds `JOB_QUEUE`/`RATE_LIMITER`/`CACHE`/`MESSAGE_BROKER` to their
110
+ * Redis-backed drivers - the broker being the `RedisStreamsBroker` reference driver,
111
+ * which an overlay (RabbitMQ/Kafka) or a test's `configure` callback can replace.
112
+ * `assertDurableSeamsBound` runs right after
113
+ * plugins + `configure` and throws a clear, actionable error listing every
114
+ * always-needed seam that still has no binding - it never falls back to an
115
+ * in-process impl. `REALTIME_TRANSPORT` stays lazy (throws on first use) since not
116
+ * every deployment serves realtime traffic. `OUTBOX_ENABLED` (or
117
+ * `AMQP_URL`/`RABBITMQ_URL`) enables the transactional outbox relay.
118
+ * `container.dispose()` (via `close()`) runs disposers in REVERSE registration
119
+ * order, so DRIZZLE is registered before JOB_QUEUE to guarantee workers drain
120
+ * before the DB closes. A router namespace registered by more than one plugin
121
+ * throws at boot, not at request time.
95
122
  */
96
123
  export async function createApp(config) {
97
124
  if (config.databaseUrl) {
@@ -103,35 +130,31 @@ export async function createApp(config) {
103
130
  container.onDispose(() => svc.dispose());
104
131
  return svc;
105
132
  });
106
- container.register(MESSAGE_BROKER, () => {
107
- const broker = new InMemoryBroker();
108
- container.onDispose(() => broker.close());
109
- return broker;
110
- });
111
- // Enabled when OUTBOX_ENABLED is set or a durable broker is configured (AMQP_URL) -
112
- // a distributed deployment wants events that survive a crash between commit and
113
- // publish. In the default in-process monolith it stays off, so emit() keeps its
114
- // synchronous fan-out and emitInTransaction() guides callers to enable it.
133
+ // A distributed deployment wants events that survive a crash between commit and
134
+ // publish. Off by default: emit() keeps its best-effort fan-out and
135
+ // emitInTransaction() throws a guiding error until this is enabled.
136
+ // AMQP_URL/RABBITMQ_URL only signal the INTENT to run a RabbitMQ broker overlay -
137
+ // core binds no AMQP driver, so they enable the outbox and nothing else. The relay
138
+ // publishes to whatever MESSAGE_BROKER resolves to (the Redis Streams driver below,
139
+ // unless an overlay replaced it); set OUTBOX_ENABLED directly when that is the aim.
115
140
  const outboxEnabled = !!process.env['OUTBOX_ENABLED'] || !!process.env['AMQP_URL'] || !!process.env['RABBITMQ_URL'];
116
141
  if (outboxEnabled) {
117
142
  container.register(OUTBOX, () => new DrizzleOutboxWriter());
118
143
  }
119
144
  container.register(EVENT_BUS, (c) => createEventBus(c.get(MESSAGE_BROKER), createLogger('event-bus'), outboxEnabled ? c.get(OUTBOX) : undefined));
120
- // Default in-process fan-out served as SSE; an overlay rebinds to a managed
121
- // vendor (Ably/GetStream) without touching modules. See ADR-0007.
122
- container.register(REALTIME_TRANSPORT, () => new InProcessRealtimeTransport());
123
- // Default is first-party SSE (no token; the session cookie authorizes the stream).
124
- // A managed-vendor overlay rebinds this to mint a per-player scoped token. See ADR-0007.
125
- container.register(REALTIME_CLIENT_AUTHORIZER, () => new SseClientAuthorizer());
126
- // When REDIS_URL is set, JOB_QUEUE, the rate limiter and cache bind to the shipped
127
- // Redis reference adapters: BullMQ-backed durable jobs (survive restarts, real cron),
128
- // distributed throttling and cross-replica cache invalidation - mirrors the AMQP_URL
129
- // outbox precedent above. Otherwise the process-local in-process defaults stay (zero
130
- // deps for dev/test). Either way a consumer overlay can rebind any of these tokens
131
- // (plugins load after). The JOB_QUEUE disposer drains in-flight jobs before the DB
132
- // closes (DRIZZLE is resolved before JOB_QUEUE below, and disposers run in reverse).
145
+ // When REDIS_URL is set, JOB_QUEUE, the rate limiter, the cache and the message
146
+ // broker bind to the shipped Redis reference adapters: BullMQ-backed durable jobs
147
+ // (survive restarts, real cron), distributed throttling, cross-replica cache
148
+ // invalidation and cross-replica events over Redis Streams. There is no in-process
149
+ // fallback (ADR-0030) - a deployment without REDIS_URL must bind these itself (an
150
+ // overlay, or a test's `configure` callback), or `assertDurableSeamsBound` below
151
+ // throws. The JOB_QUEUE disposer drains in-flight jobs before the DB closes (DRIZZLE
152
+ // is resolved before JOB_QUEUE below, and disposers run in reverse).
133
153
  const redisUrl = process.env['REDIS_URL'];
134
154
  if (redisUrl) {
155
+ // Resolved eagerly, before any client is opened: a misconfigured service name is
156
+ // a boot error, not a surprise on whichever code path resolves the broker first.
157
+ const serviceName = resolveServiceName();
135
158
  container.register(JOB_QUEUE, () => {
136
159
  const q = new BullMqJobQueue(redisUrl);
137
160
  container.onDispose(() => q.close());
@@ -141,22 +164,10 @@ export async function createApp(config) {
141
164
  container.onDispose(() => redis.close());
142
165
  container.register(RATE_LIMITER, () => new RedisRateLimiter(redis));
143
166
  container.register(CACHE, () => new RedisCache(redis));
144
- }
145
- else {
146
- container.register(JOB_QUEUE, () => {
147
- const q = new InProcessJobQueue(createLogger('job-queue'));
148
- container.onDispose(() => q.close());
149
- return q;
150
- });
151
- container.register(RATE_LIMITER, () => {
152
- const limiter = new InProcessRateLimiter();
153
- container.onDispose(() => limiter.close());
154
- return limiter;
155
- });
156
- container.register(CACHE, () => {
157
- const cache = new InProcessCache();
158
- container.onDispose(() => cache.close());
159
- return cache;
167
+ container.register(MESSAGE_BROKER, () => {
168
+ const broker = new RedisStreamsBroker(redis, { serviceName });
169
+ container.onDispose(() => broker.close());
170
+ return broker;
160
171
  });
161
172
  }
162
173
  // One shared better-auth instance for both the per-request middleware and AdminGuard -
@@ -176,6 +187,7 @@ export async function createApp(config) {
176
187
  container.register(PLATFORM_CONFIG, () => loadPlatformConfig(resolvePlatformConfigPath()));
177
188
  const registry = await loadPlugins(config.plugins, container);
178
189
  await config.configure?.(container);
190
+ assertDurableSeamsBound(container);
179
191
  if (container.has(ERROR_TRACKING)) {
180
192
  const tracker = container.get(ERROR_TRACKING);
181
193
  setErrorReporter((error, context) => tracker.captureException(error, context));