@modainteract/moda-interact-shared 0.1.0 → 0.3.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,718 @@
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
+ - OpenTelemetry tracing/metrics SDK bootstraps;
47
+ - deployment OTLP endpoint/credential wiring or observability backends;
48
+ - Render deployment configuration.
49
+
50
+ Those capabilities remain in the repository that owns the corresponding
51
+ runtime concern.
52
+
53
+ ---
54
+
55
+ # Shopify contracts
56
+
57
+ Moda Interact uses the shared Shopify package boundary so webhook producers and
58
+ background consumers do not independently invent compatible-looking event
59
+ types.
60
+
61
+ Import Shopify contracts from:
62
+
63
+ ```ts
64
+ import {
65
+ // schemas, parsers, constants and types
66
+ } from "@modainteract/moda-interact-shared/shopify";
67
+ ```
68
+
69
+ Node-only Shopify helpers are isolated under:
70
+
71
+ ```ts
72
+ import {
73
+ createShopifyWebhookJobId,
74
+ } from "@modainteract/moda-interact-shared/shopify/node";
75
+ ```
76
+
77
+ The Node-only subpath prevents `node:` built-ins from being accidentally pulled
78
+ into consumers that only need the browser-safe Shopify contract entry point.
79
+
80
+ ## Recovery-focused events
81
+
82
+ The current recovery architecture defines distinct event meanings for:
83
+
84
+ ```text
85
+ checkout.created
86
+ checkout.updated
87
+ order.completed
88
+ ```
89
+
90
+ Pre-recovery checkout events deliberately avoid transporting customer identity,
91
+ line-item, pricing and address data merely because those values existed in the
92
+ provider webhook.
93
+
94
+ Consumers must parse/validate cross-service data with the shared runtime
95
+ contracts before acting on it. TypeScript types alone are not a trust boundary.
96
+
97
+ ## Contract versioning
98
+
99
+ Serialized event schema versions and the npm package version are separate
100
+ concepts.
101
+
102
+ A breaking change to an existing serialized event contract requires an
103
+ appropriate event-schema version change and coordinated producer/consumer
104
+ rollout.
105
+
106
+ An additive npm package capability, such as a new independent package export,
107
+ does not by itself require changing an unrelated serialized event schema.
108
+
109
+ ---
110
+
111
+ # Structured logging
112
+
113
+ Moda Interact runtime services use one reusable structured logging primitive:
114
+
115
+ ```ts
116
+ import {
117
+ createLogger,
118
+ type StructuredLogger,
119
+ type LogFields,
120
+ } from "@modainteract/moda-interact-shared/logging";
121
+ ```
122
+
123
+ ## Create a logger
124
+
125
+ Each deployable service/process supplies its own identity and environment:
126
+
127
+ ```ts
128
+ const logger = createLogger({
129
+ serviceName: "moda-interact",
130
+ environment: "test",
131
+ });
132
+ ```
133
+
134
+ The default namespace is:
135
+
136
+ ```text
137
+ moda-interact
138
+ ```
139
+
140
+ Logs contain the canonical identity:
141
+
142
+ ```text
143
+ service.namespace=moda-interact
144
+ service.name=<service/process name>
145
+ deployment.environment.name=<environment>
146
+ ```
147
+
148
+ For deployed Moda Interact environments, the expected environment values are:
149
+
150
+ ```text
151
+ test
152
+ production
153
+ ```
154
+
155
+ The shared library does not read a service's environment variables for it.
156
+ The owning service resolves its configuration and passes the resulting
157
+ environment to `createLogger`.
158
+
159
+ ## Write logs
160
+
161
+ ```ts
162
+ logger.info("shopify.webhook.outcome", {
163
+ topic: "checkouts/create",
164
+ outcome: "ENQUEUED",
165
+ ackMs: 18,
166
+ });
167
+
168
+ logger.warn("queue.job.retry", {
169
+ queue: "recovery",
170
+ attempt: 2,
171
+ });
172
+
173
+ logger.error("recovery.failed", {
174
+ recoveryId,
175
+ error,
176
+ });
177
+ ```
178
+
179
+ The default sink emits one structured JSON record per console call.
180
+
181
+ Example:
182
+
183
+ ```json
184
+ {
185
+ "timestamp": "2026-08-30T12:00:00.000Z",
186
+ "level": "info",
187
+ "event": "shopify.webhook.outcome",
188
+ "service.namespace": "moda-interact",
189
+ "service.name": "moda-interact",
190
+ "deployment.environment.name": "test",
191
+ "data": {
192
+ "topic": "checkouts/create",
193
+ "outcome": "ENQUEUED",
194
+ "ackMs": 18
195
+ }
196
+ }
197
+ ```
198
+
199
+ ## Log levels
200
+
201
+ Available levels are:
202
+
203
+ ```text
204
+ debug
205
+ info
206
+ warn
207
+ error
208
+ ```
209
+
210
+ Use stable, machine-readable event names such as:
211
+
212
+ ```text
213
+ shopify.webhook.outcome
214
+ meta.webhook.outcome
215
+ queue.job.started
216
+ queue.job.completed
217
+ queue.job.failed
218
+ recovery.materialized
219
+ ```
220
+
221
+ ## Child loggers
222
+
223
+ `child()` is optional.
224
+
225
+ It creates another logger with some repeated fields already attached. It does
226
+ **not** create a worker, queue, process, connection, or logging service.
227
+
228
+ Instead of repeatedly writing:
229
+
230
+ ```ts
231
+ logger.info("queue.job.started", {
232
+ queue: "recovery",
233
+ jobId,
234
+ });
235
+
236
+ logger.info("queue.job.completed", {
237
+ queue: "recovery",
238
+ jobId,
239
+ });
240
+ ```
241
+
242
+ you may write:
243
+
244
+ ```ts
245
+ const recoveryLogger = logger.child({
246
+ queue: "recovery",
247
+ });
248
+
249
+ recoveryLogger.info("queue.job.started", {
250
+ jobId,
251
+ });
252
+
253
+ recoveryLogger.info("queue.job.completed", {
254
+ jobId,
255
+ });
256
+ ```
257
+
258
+ Both records automatically contain:
259
+
260
+ ```json
261
+ {
262
+ "queue": "recovery"
263
+ }
264
+ ```
265
+
266
+ Use child context only for stable, safe operational fields.
267
+
268
+ ## Sensitive data
269
+
270
+ The logger provides defense-in-depth redaction and bounded serialization, but
271
+ callers remain responsible for constructing safe, explicit log fields.
272
+
273
+ Do not intentionally log:
274
+
275
+ - access or refresh tokens;
276
+ - authorization headers;
277
+ - cookies;
278
+ - OAuth codes;
279
+ - passwords or API keys;
280
+ - private keys;
281
+ - webhook verification secrets;
282
+ - complete webhook/request/response payloads;
283
+ - customer names, email addresses, phone numbers or postal addresses;
284
+ - payment/card data.
285
+
286
+ Do not pass a complete provider/customer object to the logger and rely on
287
+ redaction to make it safe.
288
+
289
+ ## Errors
290
+
291
+ Errors can be logged as values:
292
+
293
+ ```ts
294
+ logger.error("queue.job.failed", {
295
+ jobId,
296
+ error,
297
+ });
298
+ ```
299
+
300
+ The shared logger safely serializes `Error` objects without emitting stack
301
+ traces by default.
302
+
303
+ Logging is best-effort: a logger sink or serialization failure must not alter
304
+ the success/failure semantics of the business operation being logged.
305
+
306
+ ## Logging destinations
307
+
308
+ Every call to the shared logger:
309
+
310
+ ```ts
311
+ logger.debug(...)
312
+ logger.info(...)
313
+ logger.warn(...)
314
+ logger.error(...)
315
+ ```
316
+
317
+ emits the same already-sanitized canonical record to up to three independent,
318
+ best-effort destinations:
319
+
320
+ 1. canonical structured JSON to the configured sink (default: console);
321
+ 2. the OpenTelemetry Logs API via the global `LoggerProvider`, if one has been
322
+ installed;
323
+ 3. a direct Grafana Loki transport, if the Node process bootstrap has installed
324
+ the Loki emitter.
325
+
326
+ Each destination is failure-isolated: a failure in one must not suppress
327
+ another and must never affect application/business correctness.
328
+
329
+ The OpenTelemetry API is a no-op when no provider is installed, so stdout
330
+ logging continues to work in local/test environments without any backend.
331
+
332
+ The heavy Node SDK/exporter and the Winston/winston-loki transport are isolated
333
+ in:
334
+
335
+ ```text
336
+ @modainteract/moda-interact-shared/logging/node
337
+ ```
338
+
339
+ and must be imported and initialized only from the Node process bootstrap —
340
+ never through Vite/application/browser bundles.
341
+
342
+ ```text
343
+ application module
344
+ |
345
+ v
346
+ @modainteract/moda-interact-shared/logging
347
+ |
348
+ +--> canonical JSON stdout
349
+ |
350
+ +--> @opentelemetry/api-logs
351
+ | |
352
+ | | global LoggerProvider
353
+ | v
354
+ | ./logging/node
355
+ | |
356
+ | +--> LoggerProvider
357
+ | +--> BatchLogRecordProcessor
358
+ | +--> OTLPLogExporter
359
+ | |
360
+ | v
361
+ | /v1/logs
362
+ |
363
+ +--> lightweight Loki bridge (globalThis emitter)
364
+ |
365
+ v
366
+ ./logging/node
367
+ |
368
+ +--> Winston + winston-loki
369
+ |
370
+ v
371
+ Grafana Loki
372
+ ```
373
+
374
+ Winston and winston-loki exist **only** in the Node-only package graph. The
375
+ normal `./logging` entry contains no Winston, winston-loki or Node networking
376
+ imports.
377
+
378
+ ### Node bootstrap
379
+
380
+ Process bootstrap code installs the optional destinations once:
381
+
382
+ ```ts
383
+ import {
384
+ initNodeLokiLogging,
385
+ initNodeOpenTelemetryLogging,
386
+ } from "@modainteract/moda-interact-shared/logging/node";
387
+
388
+ initNodeLokiLogging({
389
+ serviceName: "moda-interact",
390
+ environment: "production",
391
+ });
392
+
393
+ initNodeOpenTelemetryLogging({
394
+ serviceName: "moda-interact",
395
+ environment: "production",
396
+ });
397
+ ```
398
+
399
+ Each destination is independently configurable, so a deployment may use:
400
+
401
+ ```text
402
+ console only
403
+
404
+ console + Loki
405
+
406
+ console + OTel Logs
407
+
408
+ console + Loki + OTel Logs
409
+ ```
410
+
411
+ Either Node destination may be omitted entirely; without an installed
412
+ provider/emitter the logger remains a safe stdout-only logger.
413
+
414
+ The bootstrap supports the standard OpenTelemetry environment variables:
415
+
416
+ ```text
417
+ OTEL_SDK_DISABLED
418
+
419
+ OTEL_LOGS_EXPORTER
420
+
421
+ OTEL_EXPORTER_OTLP_LOGS_ENDPOINT
422
+ OTEL_EXPORTER_OTLP_ENDPOINT
423
+
424
+ OTEL_EXPORTER_OTLP_LOGS_HEADERS
425
+ OTEL_EXPORTER_OTLP_HEADERS
426
+
427
+ OTEL_BLRP_MAX_QUEUE_SIZE
428
+ OTEL_BLRP_MAX_EXPORT_BATCH_SIZE
429
+ OTEL_BLRP_SCHEDULE_DELAY
430
+ OTEL_BLRP_EXPORT_TIMEOUT
431
+
432
+ OTEL_LOG_EXPORT_CONCURRENCY_LIMIT
433
+ ```
434
+
435
+ Endpoint rules:
436
+
437
+ ```text
438
+ OTEL_EXPORTER_OTLP_LOGS_ENDPOINT
439
+ exact signal endpoint; do not append /v1/logs
440
+
441
+ OTEL_EXPORTER_OTLP_ENDPOINT
442
+ generic endpoint; append /v1/logs
443
+ ```
444
+
445
+ Moda deliberately does **not** export to the OpenTelemetry default localhost
446
+ endpoint. Without an explicitly configured endpoint the logger remains
447
+ stdout-only.
448
+
449
+ ### Direct Loki transport
450
+
451
+ The direct Loki transport is a lightweight bridge: the normal `./logging` entry
452
+ stores an emitter on `globalThis` and the Node bootstrap installs it. Records
453
+ still pass through the same shared redaction/sanitization used for stdout and
454
+ OpenTelemetry.
455
+
456
+ The Loki environment contract is:
457
+
458
+ ```text
459
+ LOKI_ENABLED
460
+
461
+ LOKI_URL
462
+
463
+ LOKI_USERNAME
464
+ LOKI_PASSWORD
465
+
466
+ LOKI_BATCHING
467
+ LOKI_BATCH_INTERVAL_SECONDS
468
+ LOKI_TIMEOUT_MS
469
+ LOKI_CLEAR_ON_ERROR
470
+ ```
471
+
472
+ Rules:
473
+
474
+ ```text
475
+ no LOKI_URL
476
+ -> direct Loki disabled
477
+
478
+ LOKI_ENABLED=false
479
+ -> direct Loki disabled
480
+
481
+ LOKI_URL present
482
+ -> direct Loki enabled by default
483
+ ```
484
+
485
+ Credentials are Node-bootstrap configuration only. Never log `LOKI_URL`
486
+ credentials, `LOKI_USERNAME`, `LOKI_PASSWORD`, Basic Auth values or connection
487
+ headers.
488
+
489
+ Default transport behavior:
490
+
491
+ ```text
492
+ batching=true
493
+ bounded timeout (LOKI_TIMEOUT_MS)
494
+ clearOnError=true
495
+ replaceTimestamp=true
496
+ gracefulShutdown=false
497
+ ```
498
+
499
+ The owning process bootstrap is responsible for explicit flush/shutdown via the
500
+ returned runtime (`forceFlush`/`shutdown`). If Loki is unavailable, log records
501
+ may be dropped but application processing must continue. Connection errors are
502
+ not recursively written back through the shared logger.
503
+
504
+ ### Loki labels and cardinality
505
+
506
+ The only default Loki labels are the canonical low-cardinality identity:
507
+
508
+ ```text
509
+ service_namespace
510
+ service_name
511
+ environment
512
+ ```
513
+
514
+ Dynamic/high-cardinality identifiers must **not** be promoted to Loki stream
515
+ labels. The implementation explicitly retains
516
+ `useWinstonMetaAsLabels: false`. In particular, do not label by:
517
+
518
+ ```text
519
+ traceId
520
+ spanId
521
+ requestId
522
+ jobId
523
+ deliveryId
524
+ eventId
525
+ recoveryId
526
+ checkoutToken
527
+ cartToken
528
+ shop
529
+ shopDomain
530
+ customerId
531
+ phone
532
+ email
533
+ ```
534
+
535
+ Safe high-cardinality operational identifiers may remain in the sanitized JSON
536
+ log body where justified, but they must never become indexed stream labels.
537
+
538
+ ### Duplicate-delivery warning
539
+
540
+ Do not route the OpenTelemetry Logs destination back into the same Loki
541
+ instance while direct Loki is also enabled, unless duplicate storage is
542
+ intentional:
543
+
544
+ ```text
545
+ direct Loki ON
546
+ +
547
+ OTel Logs ultimately routed to same Loki
548
+ =
549
+ duplicate log storage
550
+ ```
551
+
552
+ The library does not guess backend topology; deployment configuration owns this
553
+ choice.
554
+
555
+ ### Log-signal data safety and correlation
556
+
557
+ - The OpenTelemetry log body is `JSON.stringify` of the same canonical
558
+ sanitized `LogRecord` written to stdout, so the same redaction boundary
559
+ applies to both destinations.
560
+ - Queryable OTel attributes are limited to:
561
+
562
+ ```text
563
+ event.name
564
+ log.level
565
+ service.namespace
566
+ service.name
567
+ deployment.environment.name
568
+ ```
569
+
570
+ - The shared logger never manufactures trace IDs. If an active
571
+ trace/span context exists, the OpenTelemetry Logs SDK associates the emitted
572
+ record with that active context automatically.
573
+ - stdout and OpenTelemetry emission are isolated and best-effort: a failure in
574
+ either must not change application/business correctness.
575
+
576
+ ### Tracing/metrics remain service-specific
577
+
578
+ The shared logger does **not** create spans, counters or histograms.
579
+
580
+ Service-specific OpenTelemetry tracing and metrics remain in the owning runtime
581
+ repository, for example:
582
+
583
+ ```text
584
+ Shopify operation
585
+ |
586
+ +--> shared structured logger (stdout + OTel Logs)
587
+ |
588
+ +--> Shopify-specific OpenTelemetry (spans/metrics)
589
+ ```
590
+
591
+ This separation is intentional. Fields that are useful in diagnostic logs can
592
+ have very different cardinality constraints from metric attributes.
593
+
594
+ ---
595
+
596
+ # Package exports
597
+
598
+ The public package exposes independent entry points.
599
+
600
+ ```text
601
+ @modainteract/moda-interact-shared
602
+ existing root exports
603
+
604
+ @modainteract/moda-interact-shared/shopify
605
+ Shopify cross-service contracts, schemas, constants and pure helpers
606
+
607
+ @modainteract/moda-interact-shared/shopify/node
608
+ Node-only Shopify helpers
609
+
610
+ @modainteract/moda-interact-shared/logging
611
+ reusable structured logging API
612
+ (stdout + OTel Logs + direct Loki fan-out)
613
+
614
+ @modainteract/moda-interact-shared/logging/node
615
+ Node-only logging bootstrap
616
+ (OpenTelemetry LoggerProvider, batching, OTLP HTTP exporter;
617
+ Winston + winston-loki direct transport)
618
+ ```
619
+
620
+ Consumers should prefer the narrowest appropriate subpath.
621
+
622
+ ---
623
+
624
+ # Ownership boundary
625
+
626
+ Examples of repository ownership:
627
+
628
+ **moda-interact**
629
+
630
+ - authenticates Shopify ingress;
631
+ - normalizes/validates Shopify events;
632
+ - publishes the appropriate queue jobs;
633
+ - uses the shared structured logger for generic log mechanics;
634
+ - owns Shopify-specific observability semantics and OpenTelemetry.
635
+
636
+ **moda-interact-background**
637
+
638
+ - parses shared cross-service contracts before acting;
639
+ - runs BullMQ workers and recovery workflows;
640
+ - uses the shared logger for generic structured logging;
641
+ - owns worker/recovery OpenTelemetry.
642
+
643
+ **moda-interact-messaging**
644
+
645
+ - owns Meta/WhatsApp ingress;
646
+ - uses the shared logger for generic structured logging;
647
+ - owns Meta/WhatsApp-specific observability and OpenTelemetry.
648
+
649
+ **moda-interact-admin**
650
+
651
+ - owns the internal admin application;
652
+ - may use the same shared logger where server-side operational logging is
653
+ required;
654
+ - owns admin-specific application telemetry.
655
+
656
+ **moda-interact-database**
657
+
658
+ - owns Prisma schema/migrations and database artifacts;
659
+ - does not duplicate shared TypeScript transport contracts.
660
+
661
+ ---
662
+
663
+ # Development
664
+
665
+ ## Typecheck
666
+
667
+ ```bash
64
668
  npm run typecheck
669
+ ```
670
+
671
+ ## Build
672
+
673
+ ```bash
65
674
  npm run build
675
+ ```
676
+
677
+ ## Test
678
+
679
+ ```bash
66
680
  npm test
67
681
  ```
682
+
683
+ ## Inspect the publish artifact
684
+
685
+ Before publishing:
686
+
687
+ ```bash
688
+ npm pack --dry-run
689
+ ```
690
+
691
+ Verify that the intended JavaScript and type declarations for every public
692
+ subpath are included.
693
+
694
+ ---
695
+
696
+ # Release policy
697
+
698
+ The npm package version follows semantic versioning.
699
+
700
+ Examples:
701
+
702
+ ```text
703
+ patch
704
+ backwards-compatible fix to an existing package capability
705
+
706
+ minor
707
+ backwards-compatible new public capability/export
708
+
709
+ major
710
+ breaking public API/package compatibility change
711
+ ```
712
+
713
+ The addition of the independent `./logging` public API is an additive package
714
+ capability and should therefore be released as a **minor** version increment.
715
+
716
+ Publishing the package and changing consuming services are coordinated tasks;
717
+ a consuming service must not assume an unpublished local shared export exists
718
+ in the npm artifact it installs.