@modainteract/moda-interact-shared 0.1.0 → 0.2.0

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/README.md CHANGED
@@ -1,67 +1,447 @@
1
1
  # @modainteract/moda-interact-shared
2
2
 
3
- Shared, runtime-validated TypeScript contracts used by more than one Moda
4
- Interact service. This package is a library, not a service: it has no
5
- transport, persistence, or framework code of its own.
3
+ Shared TypeScript contracts and reusable platform primitives used by multiple
4
+ Moda Interact services.
5
+
6
+ This package is a **library**, not a deployable service. It does not own
7
+ transport, persistence, background workers, provider SDK integration, or
8
+ application business workflows.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install @modainteract/moda-interact-shared
14
+ ```
15
+
16
+ Production services should use the exact package version selected by the
17
+ corresponding Moda Interact architecture/release task rather than relying on a
18
+ floating version.
6
19
 
7
20
  ## What this package owns
8
21
 
9
- - Zod schemas for cross-service event contracts (currently: Shopify commerce
10
- events).
11
- - TypeScript types inferred from those schemas (`z.infer`), so producers and
12
- consumers share one source of truth.
13
- - Constants (queue/job names, schema version, event type strings).
14
- - Parsing helpers and small pure utilities (ordering-key and job-id
15
- derivation) needed to construct or validate those contracts.
16
-
17
- This package does **not** define Prisma models, own migrations, or implement
18
- any queue/transport client. PostgreSQL and Redis store the *serialized*
19
- event envelope as JSON; the database layer does not enforce or duplicate its
20
- shape. Producers must construct and validate an event with the schemas in
21
- this package before persisting it. Consumers must parse the JSON read back
22
- from PostgreSQL/Redis with the same schemas before acting on it — never trust
23
- it as pre-validated.
24
-
25
- ## Ownership boundary
26
-
27
- - **moda-interact** — authenticates Shopify webhooks, normalizes payloads,
28
- constructs and validates a `ShopifyCommerceEvent`, and writes it to the
29
- transactional outbox.
30
- - **moda-interact-background** — parses `ShopifyCommerceEvent` (via
31
- `parseShopifyCommerceEvent` / `safeParseShopifyCommerceEvent`) before
32
- processing, dispatches by `eventType`, and applies tenant-safe, idempotent
33
- domain transitions.
34
- - **moda-interact-database** — stores receipt metadata and outbox JSON. It
35
- does not own or duplicate the TypeScript event types defined here.
36
-
37
- ## Schema versioning
38
-
39
- `schemaVersion` is embedded in every serialized event and is currently `1`.
40
-
41
- - Any breaking change to the serialized shape (removing/renaming a field,
42
- changing a type, tightening validation in a way that rejects previously
43
- valid data) requires bumping `SHOPIFY_COMMERCE_EVENT_SCHEMA_VERSION` and
44
- updating the schema to branch on it.
45
- - Additive, optional-field changes do not require a version bump, but must
46
- still be reviewed for old-consumer safety (an older consumer parsing a
47
- newer envelope must not fail just because of a new optional field).
48
-
49
- ## Exports
50
-
51
- - `@modainteract/moda-interact-shared` — re-exports everything from
52
- `./shopify`.
53
- - `@modainteract/moda-interact-shared/shopify` — schemas, types, constants,
54
- and the pure `createShopifyCommerceOrderingKey` helper. Safe for browser
55
- bundles (no Node built-ins).
56
- - `@modainteract/moda-interact-shared/shopify/node` — `createShopifyWebhookJobId`,
57
- which uses `node:crypto` and is therefore isolated in a Node-only subpath so
58
- it isn't accidentally pulled into browser bundles that import the root or
59
- `./shopify` entry points.
60
-
61
- ## Building and testing
62
-
63
- ```sh
22
+ The package contains code that genuinely belongs at a cross-service boundary,
23
+ including:
24
+
25
+ - runtime-validated cross-service event contracts;
26
+ - TypeScript types shared by producers and consumers;
27
+ - schema and event-version constants;
28
+ - canonical queue/job-name constants where multiple repositories must agree;
29
+ - deterministic correlation/job identifier helpers;
30
+ - small pure utilities used by more than one service;
31
+ - reusable structured application logging primitives.
32
+
33
+ The package deliberately keeps framework and runtime dependencies small.
34
+
35
+ ## What this package does not own
36
+
37
+ This package does **not** own:
38
+
39
+ - Prisma models or database migrations;
40
+ - PostgreSQL persistence;
41
+ - BullMQ/Redis clients or worker processes;
42
+ - Shopify or Meta webhook HTTP handlers;
43
+ - Shopify or Meta SDK integration;
44
+ - application recovery/business logic;
45
+ - OpenTelemetry spans or application-specific metrics;
46
+ - OTLP transport or observability backends;
47
+ - Render deployment configuration.
48
+
49
+ Those capabilities remain in the repository that owns the corresponding
50
+ runtime concern.
51
+
52
+ ---
53
+
54
+ # Shopify contracts
55
+
56
+ Moda Interact uses the shared Shopify package boundary so webhook producers and
57
+ background consumers do not independently invent compatible-looking event
58
+ types.
59
+
60
+ Import Shopify contracts from:
61
+
62
+ ```ts
63
+ import {
64
+ // schemas, parsers, constants and types
65
+ } from "@modainteract/moda-interact-shared/shopify";
66
+ ```
67
+
68
+ Node-only Shopify helpers are isolated under:
69
+
70
+ ```ts
71
+ import {
72
+ createShopifyWebhookJobId,
73
+ } from "@modainteract/moda-interact-shared/shopify/node";
74
+ ```
75
+
76
+ The Node-only subpath prevents `node:` built-ins from being accidentally pulled
77
+ into consumers that only need the browser-safe Shopify contract entry point.
78
+
79
+ ## Recovery-focused events
80
+
81
+ The current recovery architecture defines distinct event meanings for:
82
+
83
+ ```text
84
+ checkout.created
85
+ checkout.updated
86
+ order.completed
87
+ ```
88
+
89
+ Pre-recovery checkout events deliberately avoid transporting customer identity,
90
+ line-item, pricing and address data merely because those values existed in the
91
+ provider webhook.
92
+
93
+ Consumers must parse/validate cross-service data with the shared runtime
94
+ contracts before acting on it. TypeScript types alone are not a trust boundary.
95
+
96
+ ## Contract versioning
97
+
98
+ Serialized event schema versions and the npm package version are separate
99
+ concepts.
100
+
101
+ A breaking change to an existing serialized event contract requires an
102
+ appropriate event-schema version change and coordinated producer/consumer
103
+ rollout.
104
+
105
+ An additive npm package capability, such as a new independent package export,
106
+ does not by itself require changing an unrelated serialized event schema.
107
+
108
+ ---
109
+
110
+ # Structured logging
111
+
112
+ Moda Interact runtime services use one reusable structured logging primitive:
113
+
114
+ ```ts
115
+ import {
116
+ createLogger,
117
+ type StructuredLogger,
118
+ type LogFields,
119
+ } from "@modainteract/moda-interact-shared/logging";
120
+ ```
121
+
122
+ ## Create a logger
123
+
124
+ Each deployable service/process supplies its own identity and environment:
125
+
126
+ ```ts
127
+ const logger = createLogger({
128
+ serviceName: "moda-interact",
129
+ environment: "test",
130
+ });
131
+ ```
132
+
133
+ The default namespace is:
134
+
135
+ ```text
136
+ moda-interact
137
+ ```
138
+
139
+ Logs contain the canonical identity:
140
+
141
+ ```text
142
+ service.namespace=moda-interact
143
+ service.name=<service/process name>
144
+ deployment.environment.name=<environment>
145
+ ```
146
+
147
+ For deployed Moda Interact environments, the expected environment values are:
148
+
149
+ ```text
150
+ test
151
+ production
152
+ ```
153
+
154
+ The shared library does not read a service's environment variables for it.
155
+ The owning service resolves its configuration and passes the resulting
156
+ environment to `createLogger`.
157
+
158
+ ## Write logs
159
+
160
+ ```ts
161
+ logger.info("shopify.webhook.outcome", {
162
+ topic: "checkouts/create",
163
+ outcome: "ENQUEUED",
164
+ ackMs: 18,
165
+ });
166
+
167
+ logger.warn("queue.job.retry", {
168
+ queue: "recovery",
169
+ attempt: 2,
170
+ });
171
+
172
+ logger.error("recovery.failed", {
173
+ recoveryId,
174
+ error,
175
+ });
176
+ ```
177
+
178
+ The default sink emits one structured JSON record per console call.
179
+
180
+ Example:
181
+
182
+ ```json
183
+ {
184
+ "timestamp": "2026-08-30T12:00:00.000Z",
185
+ "level": "info",
186
+ "event": "shopify.webhook.outcome",
187
+ "service.namespace": "moda-interact",
188
+ "service.name": "moda-interact",
189
+ "deployment.environment.name": "test",
190
+ "data": {
191
+ "topic": "checkouts/create",
192
+ "outcome": "ENQUEUED",
193
+ "ackMs": 18
194
+ }
195
+ }
196
+ ```
197
+
198
+ ## Log levels
199
+
200
+ Available levels are:
201
+
202
+ ```text
203
+ debug
204
+ info
205
+ warn
206
+ error
207
+ ```
208
+
209
+ Use stable, machine-readable event names such as:
210
+
211
+ ```text
212
+ shopify.webhook.outcome
213
+ meta.webhook.outcome
214
+ queue.job.started
215
+ queue.job.completed
216
+ queue.job.failed
217
+ recovery.materialized
218
+ ```
219
+
220
+ ## Child loggers
221
+
222
+ `child()` is optional.
223
+
224
+ It creates another logger with some repeated fields already attached. It does
225
+ **not** create a worker, queue, process, connection, or logging service.
226
+
227
+ Instead of repeatedly writing:
228
+
229
+ ```ts
230
+ logger.info("queue.job.started", {
231
+ queue: "recovery",
232
+ jobId,
233
+ });
234
+
235
+ logger.info("queue.job.completed", {
236
+ queue: "recovery",
237
+ jobId,
238
+ });
239
+ ```
240
+
241
+ you may write:
242
+
243
+ ```ts
244
+ const recoveryLogger = logger.child({
245
+ queue: "recovery",
246
+ });
247
+
248
+ recoveryLogger.info("queue.job.started", {
249
+ jobId,
250
+ });
251
+
252
+ recoveryLogger.info("queue.job.completed", {
253
+ jobId,
254
+ });
255
+ ```
256
+
257
+ Both records automatically contain:
258
+
259
+ ```json
260
+ {
261
+ "queue": "recovery"
262
+ }
263
+ ```
264
+
265
+ Use child context only for stable, safe operational fields.
266
+
267
+ ## Sensitive data
268
+
269
+ The logger provides defense-in-depth redaction and bounded serialization, but
270
+ callers remain responsible for constructing safe, explicit log fields.
271
+
272
+ Do not intentionally log:
273
+
274
+ - access or refresh tokens;
275
+ - authorization headers;
276
+ - cookies;
277
+ - OAuth codes;
278
+ - passwords or API keys;
279
+ - private keys;
280
+ - webhook verification secrets;
281
+ - complete webhook/request/response payloads;
282
+ - customer names, email addresses, phone numbers or postal addresses;
283
+ - payment/card data.
284
+
285
+ Do not pass a complete provider/customer object to the logger and rely on
286
+ redaction to make it safe.
287
+
288
+ ## Errors
289
+
290
+ Errors can be logged as values:
291
+
292
+ ```ts
293
+ logger.error("queue.job.failed", {
294
+ jobId,
295
+ error,
296
+ });
297
+ ```
298
+
299
+ The shared logger safely serializes `Error` objects without emitting stack
300
+ traces by default.
301
+
302
+ Logging is best-effort: a logger sink or serialization failure must not alter
303
+ the success/failure semantics of the business operation being logged.
304
+
305
+ ## Logging and OpenTelemetry are separate
306
+
307
+ The shared logger does **not** automatically create:
308
+
309
+ - spans;
310
+ - counters;
311
+ - histograms;
312
+ - OTLP exports.
313
+
314
+ Service-specific OpenTelemetry remains in the owning runtime repository.
315
+
316
+ For example:
317
+
318
+ ```text
319
+ Shopify operation
320
+ |
321
+ +--> shared structured logger
322
+ |
323
+ +--> Shopify-specific OpenTelemetry
324
+ ```
325
+
326
+ This separation is intentional. Fields that are useful in diagnostic logs can
327
+ have very different cardinality constraints from metric attributes.
328
+
329
+ ---
330
+
331
+ # Package exports
332
+
333
+ The public package exposes independent entry points.
334
+
335
+ ```text
336
+ @modainteract/moda-interact-shared
337
+ existing root exports
338
+
339
+ @modainteract/moda-interact-shared/shopify
340
+ Shopify cross-service contracts, schemas, constants and pure helpers
341
+
342
+ @modainteract/moda-interact-shared/shopify/node
343
+ Node-only Shopify helpers
344
+
345
+ @modainteract/moda-interact-shared/logging
346
+ reusable structured logging API
347
+ ```
348
+
349
+ Consumers should prefer the narrowest appropriate subpath.
350
+
351
+ ---
352
+
353
+ # Ownership boundary
354
+
355
+ Examples of repository ownership:
356
+
357
+ **moda-interact**
358
+
359
+ - authenticates Shopify ingress;
360
+ - normalizes/validates Shopify events;
361
+ - publishes the appropriate queue jobs;
362
+ - uses the shared structured logger for generic log mechanics;
363
+ - owns Shopify-specific observability semantics and OpenTelemetry.
364
+
365
+ **moda-interact-background**
366
+
367
+ - parses shared cross-service contracts before acting;
368
+ - runs BullMQ workers and recovery workflows;
369
+ - uses the shared logger for generic structured logging;
370
+ - owns worker/recovery OpenTelemetry.
371
+
372
+ **moda-interact-messaging**
373
+
374
+ - owns Meta/WhatsApp ingress;
375
+ - uses the shared logger for generic structured logging;
376
+ - owns Meta/WhatsApp-specific observability and OpenTelemetry.
377
+
378
+ **moda-interact-admin**
379
+
380
+ - owns the internal admin application;
381
+ - may use the same shared logger where server-side operational logging is
382
+ required;
383
+ - owns admin-specific application telemetry.
384
+
385
+ **moda-interact-database**
386
+
387
+ - owns Prisma schema/migrations and database artifacts;
388
+ - does not duplicate shared TypeScript transport contracts.
389
+
390
+ ---
391
+
392
+ # Development
393
+
394
+ ## Typecheck
395
+
396
+ ```bash
64
397
  npm run typecheck
398
+ ```
399
+
400
+ ## Build
401
+
402
+ ```bash
65
403
  npm run build
404
+ ```
405
+
406
+ ## Test
407
+
408
+ ```bash
66
409
  npm test
67
410
  ```
411
+
412
+ ## Inspect the publish artifact
413
+
414
+ Before publishing:
415
+
416
+ ```bash
417
+ npm pack --dry-run
418
+ ```
419
+
420
+ Verify that the intended JavaScript and type declarations for every public
421
+ subpath are included.
422
+
423
+ ---
424
+
425
+ # Release policy
426
+
427
+ The npm package version follows semantic versioning.
428
+
429
+ Examples:
430
+
431
+ ```text
432
+ patch
433
+ backwards-compatible fix to an existing package capability
434
+
435
+ minor
436
+ backwards-compatible new public capability/export
437
+
438
+ major
439
+ breaking public API/package compatibility change
440
+ ```
441
+
442
+ The addition of the independent `./logging` public API is an additive package
443
+ capability and should therefore be released as a **minor** version increment.
444
+
445
+ Publishing the package and changing consuming services are coordinated tasks;
446
+ a consuming service must not assume an unpublished local shared export exists
447
+ in the npm artifact it installs.