@endora-commerce/mod-google-tag-manager 0.100.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.
Files changed (34) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/dist/backend/index.d.ts +62 -0
  4. package/dist/backend/index.d.ts.map +1 -0
  5. package/dist/backend/index.js +70 -0
  6. package/dist/backend/index.js.map +1 -0
  7. package/dist/backend/routes.storefront.d.ts +16 -0
  8. package/dist/backend/routes.storefront.d.ts.map +1 -0
  9. package/dist/backend/routes.storefront.js +40 -0
  10. package/dist/backend/routes.storefront.js.map +1 -0
  11. package/dist/backend/services/gtm-config.service.d.ts +27 -0
  12. package/dist/backend/services/gtm-config.service.d.ts.map +1 -0
  13. package/dist/backend/services/gtm-config.service.js +65 -0
  14. package/dist/backend/services/gtm-config.service.js.map +1 -0
  15. package/dist/backend/services/sgtm-client.d.ts +46 -0
  16. package/dist/backend/services/sgtm-client.d.ts.map +1 -0
  17. package/dist/backend/services/sgtm-client.js +69 -0
  18. package/dist/backend/services/sgtm-client.js.map +1 -0
  19. package/dist/backend/services/ss-relay-queue.d.ts +46 -0
  20. package/dist/backend/services/ss-relay-queue.d.ts.map +1 -0
  21. package/dist/backend/services/ss-relay-queue.js +29 -0
  22. package/dist/backend/services/ss-relay-queue.js.map +1 -0
  23. package/dist/backend/services/ss-relay.service.d.ts +24 -0
  24. package/dist/backend/services/ss-relay.service.d.ts.map +1 -0
  25. package/dist/backend/services/ss-relay.service.js +77 -0
  26. package/dist/backend/services/ss-relay.service.js.map +1 -0
  27. package/dist/manifest.d.ts +194 -0
  28. package/dist/manifest.d.ts.map +1 -0
  29. package/dist/manifest.js +117 -0
  30. package/dist/manifest.js.map +1 -0
  31. package/docs/google-tag-manager.md +228 -0
  32. package/i18n/en.json +4 -0
  33. package/i18n/pl.json +4 -0
  34. package/package.json +64 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Endora sp. z o.o. and the Endora Commerce contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ <!-- Generated by `pnpm --filter backend run manifests:generate`. Delete this line to take the file over; a README without it is never regenerated. -->
2
+
3
+ # @endora-commerce/mod-google-tag-manager
4
+
5
+ Google Tag Manager integration: per-sales-channel container injection behind the storefront consent decision, a documented commerce dataLayer vocabulary, and an optional server-side tagging relay.
6
+
7
+ ## What this is
8
+
9
+ An **Endora Commerce module package**. Its module id is `google_tag_manager` — the identity of record in the platform's registries, in its migrations and in its setting, permission and translation codes.
10
+
11
+ A module is not imported by application code. The platform discovers the extension packages an instance has installed, reads the manifest on the root subpath and composes the module from it, so installing this package and starting the instance is the whole integration.
12
+
13
+ ## Entry points
14
+
15
+ | Import | Contents |
16
+ | --- | --- |
17
+ | `@endora-commerce/mod-google-tag-manager` | the module manifest — its id, version, dependencies, settings and activation |
18
+ | `@endora-commerce/mod-google-tag-manager/backend` | the composition root the platform calls, with the entities, services, routes and workers it registers |
19
+
20
+ ## Depends on
21
+
22
+ Everything below is a **peer** dependency, so the application resolves exactly one copy of each — two copies of a host or of React are a runtime failure rather than a type error. An entry marked *optional* is needed only by the layers that use it.
23
+
24
+ **Endora packages**
25
+
26
+ - `@endora-commerce/contracts`
27
+ - `@endora-commerce/platform`
28
+
29
+ **Third-party**
30
+
31
+ - `bullmq` ^5
32
+ - `fastify` ^5
33
+ - `ioredis` ^5
34
+ - `zod` ^4
35
+
36
+ ## What the tarball carries
37
+
38
+ - `dist/` — the compiled JavaScript and its type declarations
39
+ - `i18n/` — the translation bundles this package ships (`en`, `pl`), loaded by the platform
40
+ - `docs/` — the package’s own pages in the Endora Commerce documentation site
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ pnpm add @endora-commerce/mod-google-tag-manager
46
+ ```
47
+
48
+ ## Licence
49
+
50
+ MIT — the text is in `LICENSE`, beside this file.
@@ -0,0 +1,62 @@
1
+ import type { Redis } from 'ioredis';
2
+ import type { ModuleContext } from '@endora-commerce/platform/kernel';
3
+ import type { SettingsReadPort } from '@endora-commerce/platform/kernel';
4
+ import { StorefrontRevalidator } from '@endora-commerce/platform/http';
5
+ import { GtmConfigService } from './services/gtm-config.service.js';
6
+ import { makeEnqueuer } from './services/ss-relay.service.js';
7
+ /**
8
+ * `google_tag_manager` — the third of the four ad modules whose subscription
9
+ * outlived its gate (feature 072, wave 2, T103).
10
+ *
11
+ * Same shape as `meta_ads` and `linkedin_ads`: routes wrapped in
12
+ * `defineModuleRoutes`, but `onSettingChanged` arriving from a root as a bare
13
+ * `eventBus.on('settings.value_changed', …)`, so a switched-off module still
14
+ * answered every `google_tag_manager.*` change with an outbound storefront
15
+ * revalidation. `ctx.subscribe` is `subscribeForModule`, so it stops with the
16
+ * module now.
17
+ *
18
+ * **`moduleQueueRedis` is a separate name from `redis`, on purpose.** This
19
+ * module builds a BullMQ producer queue when it has a Redis connection, and the
20
+ * harness deliberately gives it none — `test-server.ts` says why: "a BullMQ
21
+ * queue built per `setupBackendServer()` is never closed, and this harness is
22
+ * constructed once per test file inside a single fork", so `/collect` degrades
23
+ * to 503 there and is contract-tested against its own bare instance instead.
24
+ * `redis` *is* registered in the harness container, so resolving that would
25
+ * have silently handed this module a connection the harness spent a comment
26
+ * explaining it must not have. A distinct name lets a composition say "no queue
27
+ * here" instead of leaving it to a missing option.
28
+ *
29
+ * The worker is registered through `ctx.worker`, which is `defineModuleWorker`
30
+ * — so the consumer stops with the module too, not only the producer.
31
+ */
32
+ interface GtmServices {
33
+ readonly revalidator: StorefrontRevalidator;
34
+ readonly configService: GtmConfigService;
35
+ readonly invalidateConfig: () => void;
36
+ readonly enqueueRelay: ReturnType<typeof makeEnqueuer> | undefined;
37
+ }
38
+ export interface GoogleTagManagerCradle {
39
+ readonly settingsReadPort: SettingsReadPort;
40
+ /**
41
+ * The connection this module may build its relay queue on. Absent in a
42
+ * composition that does not want a queue — which is the harness, and any
43
+ * deployment running without Redis.
44
+ */
45
+ readonly moduleQueueRedis: Redis | undefined;
46
+ readonly googleTagManagerServices: GtmServices;
47
+ }
48
+ export declare function registerModule(ctx: ModuleContext): void;
49
+ /**
50
+ * The module owns no persisted entity class, so the array is empty, on the `./backend` subpath, as one
51
+ * array and **no named class export** (D-168).
52
+ *
53
+ * This is the shape the platform reads when the package is *installed*: the
54
+ * boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
55
+ * and the static declaration reader (`scripts/lib/package-declarations.ts`),
56
+ * which is the third source of `check:module-boundary`'s `table→owner` map and
57
+ * the package pass of `check-entity-tenant-classification`. A missing array is
58
+ * answered with `[]` — zero entities registered, no error anywhere.
59
+ */
60
+ export declare const entities: readonly never[];
61
+ export {};
62
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AAEtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAEpE,OAAO,EAAE,YAAY,EAAiB,MAAM,gCAAgC,CAAC;AAI7E;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,UAAU,WAAW;IACnB,QAAQ,CAAC,WAAW,EAAE,qBAAqB,CAAC;IAC5C,QAAQ,CAAC,aAAa,EAAE,gBAAgB,CAAC;IACzC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,IAAI,CAAC;IACtC,QAAQ,CAAC,YAAY,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,GAAG,SAAS,CAAC;CACpE;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,CAAC;IAC5C;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,KAAK,GAAG,SAAS,CAAC;IAC7C,QAAQ,CAAC,wBAAwB,EAAE,WAAW,CAAC;CAChD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CA6DvD;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,EAAE,SAAS,KAAK,EAAO,CAAC"}
@@ -0,0 +1,70 @@
1
+ import { lazyPort } from '@endora-commerce/platform/kernel';
2
+ import { StorefrontRevalidator } from '@endora-commerce/platform/http';
3
+ import { GtmConfigService } from './services/gtm-config.service.js';
4
+ import { createGtmRelayQueue, createGtmRelayWorker } from './services/ss-relay-queue.js';
5
+ import { makeEnqueuer, makeProcessor } from './services/ss-relay.service.js';
6
+ import { SgtmClient } from './services/sgtm-client.js';
7
+ import { registerGoogleTagManagerStorefrontRoutes } from './routes.storefront.js';
8
+ export function registerModule(ctx) {
9
+ ctx.di.register({
10
+ googleTagManagerServices: ctx
11
+ .asFunction(({ moduleQueueRedis }) => {
12
+ const revalidator = new StorefrontRevalidator({
13
+ baseUrl: process.env['STOREFRONT_BASE_URL'],
14
+ secret: process.env['REVALIDATE_SECRET'],
15
+ });
16
+ const invalidateConfig = () => {
17
+ void revalidator.revalidate(['gtm:config']);
18
+ };
19
+ const configService = new GtmConfigService(lazyPort(ctx, 'settingsReadPort'));
20
+ // Producer side: the API needs it to enqueue whether or not this
21
+ // process also runs the consumer.
22
+ const relayQueue = moduleQueueRedis
23
+ ? createGtmRelayQueue(moduleQueueRedis)
24
+ : undefined;
25
+ return {
26
+ revalidator,
27
+ configService,
28
+ invalidateConfig,
29
+ enqueueRelay: relayQueue ? makeEnqueuer(relayQueue) : undefined,
30
+ };
31
+ })
32
+ .singleton(),
33
+ });
34
+ // An operator's edit propagates immediately rather than after the 300 s TTL
35
+ // (FR-009) — while the module is on.
36
+ ctx.subscribe('settings.value_changed', (payload) => {
37
+ const settingCode = payload.settingCode;
38
+ if (typeof settingCode !== 'string' || !settingCode.startsWith('google_tag_manager.'))
39
+ return;
40
+ ctx.cradle().googleTagManagerServices.invalidateConfig();
41
+ });
42
+ ctx.routes(async (app) => {
43
+ const { googleTagManagerServices, settingsReadPort, moduleQueueRedis } = ctx.cradle();
44
+ googleTagManagerServices.revalidator.setLogger(app.log);
45
+ // Queue consumer (Principle X): a separable worker entrypoint, gated on
46
+ // role. `ctx.worker` wraps it in `defineModuleWorker`.
47
+ if (process.env['BACKEND_ROLE'] !== 'api' && moduleQueueRedis) {
48
+ ctx.worker(createGtmRelayWorker(moduleQueueRedis, makeProcessor({ settings: settingsReadPort, client: new SgtmClient() })), { logger: app.log });
49
+ }
50
+ await registerGoogleTagManagerStorefrontRoutes(app, {
51
+ configService: googleTagManagerServices.configService,
52
+ ...(googleTagManagerServices.enqueueRelay
53
+ ? { enqueueRelay: googleTagManagerServices.enqueueRelay }
54
+ : {}),
55
+ });
56
+ });
57
+ }
58
+ /**
59
+ * The module owns no persisted entity class, so the array is empty, on the `./backend` subpath, as one
60
+ * array and **no named class export** (D-168).
61
+ *
62
+ * This is the shape the platform reads when the package is *installed*: the
63
+ * boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
64
+ * and the static declaration reader (`scripts/lib/package-declarations.ts`),
65
+ * which is the third source of `check:module-boundary`'s `table→owner` map and
66
+ * the package pass of `check-entity-tenant-classification`. A missing array is
67
+ * answered with `[]` — zero entities registered, no error anywhere.
68
+ */
69
+ export const entities = [];
70
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { GtmConfigService } from './services/gtm-config.service.js';
3
+ import type { GtmCollectEnqueuer } from './services/ss-relay-queue.js';
4
+ export interface GoogleTagManagerStorefrontDeps {
5
+ configService: GtmConfigService;
6
+ /** Producer that enqueues server-side relay jobs (US3). Absent ⇒ 503. */
7
+ enqueueRelay?: GtmCollectEnqueuer;
8
+ }
9
+ /**
10
+ * Public storefront routes for the Google Tag Manager module (feature 066).
11
+ * Wrapped by `defineModuleRoutes` in plugin.ts so they 503 when the module is
12
+ * disabled. The sales channel comes from the shared resolver middleware
13
+ * (Principle XII) — never from the request body.
14
+ */
15
+ export declare function registerGoogleTagManagerStorefrontRoutes(app: FastifyInstance, deps: GoogleTagManagerStorefrontDeps): Promise<void>;
16
+ //# sourceMappingURL=routes.storefront.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.storefront.d.ts","sourceRoot":"","sources":["../../src/backend/routes.storefront.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAEvE,MAAM,WAAW,8BAA8B;IAC7C,aAAa,EAAE,gBAAgB,CAAC;IAChC,yEAAyE;IACzE,YAAY,CAAC,EAAE,kBAAkB,CAAC;CACnC;AAED;;;;;GAKG;AACH,wBAAsB,wCAAwC,CAC5D,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,8BAA8B,GACnC,OAAO,CAAC,IAAI,CAAC,CA+Bf"}
@@ -0,0 +1,40 @@
1
+ import { gtmCollectRequestSchema } from '@endora-commerce/contracts';
2
+ import { getResolvedChannel } from '@endora-commerce/platform/kernel';
3
+ /**
4
+ * Public storefront routes for the Google Tag Manager module (feature 066).
5
+ * Wrapped by `defineModuleRoutes` in plugin.ts so they 503 when the module is
6
+ * disabled. The sales channel comes from the shared resolver middleware
7
+ * (Principle XII) — never from the request body.
8
+ */
9
+ export async function registerGoogleTagManagerStorefrontRoutes(app, deps) {
10
+ app.get('/api/v1/storefront/google-tag-manager/config', async (request, reply) => {
11
+ const channel = getResolvedChannel(request);
12
+ const config = await deps.configService.getConfig(channel.id);
13
+ return reply.send({ data: config });
14
+ });
15
+ // Server-side relay ingest — producer only (Principle X): validate → enqueue
16
+ // → 202. Nothing is forwarded inside the shopper's request (FR-029).
17
+ app.post('/api/v1/storefront/google-tag-manager/collect', async (request, reply) => {
18
+ if (!deps.enqueueRelay) {
19
+ return reply.status(503).send({
20
+ error: {
21
+ code: 'server_side_unavailable',
22
+ message: 'Server-side tagging is not available.',
23
+ },
24
+ });
25
+ }
26
+ const channel = getResolvedChannel(request);
27
+ // The schema is the enforcement point for the closed event allow-list: an
28
+ // event name outside it is a 400 and is never enqueued (FR-024 / FR-025).
29
+ const body = gtmCollectRequestSchema.parse(request.body);
30
+ // Read from the connection and the headers, never from the body — a client
31
+ // must not be able to nominate someone else's IP or user agent.
32
+ const userAgent = request.headers['user-agent'];
33
+ const accepted = await deps.enqueueRelay(channel.id, body, {
34
+ ...(request.ip ? { ip: request.ip } : {}),
35
+ ...(userAgent ? { userAgent } : {}),
36
+ });
37
+ return reply.status(202).send({ data: { accepted } });
38
+ });
39
+ }
40
+ //# sourceMappingURL=routes.storefront.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.storefront.js","sourceRoot":"","sources":["../../src/backend/routes.storefront.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAUtE;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,wCAAwC,CAC5D,GAAoB,EACpB,IAAoC;IAEpC,GAAG,CAAC,GAAG,CAAC,8CAA8C,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QAC/E,MAAM,OAAO,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;QAC5C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC9D,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,6EAA6E;IAC7E,qEAAqE;IACrE,GAAG,CAAC,IAAI,CAAC,+CAA+C,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACjF,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;YACvB,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBAC5B,KAAK,EAAE;oBACL,IAAI,EAAE,yBAAyB;oBAC/B,OAAO,EAAE,uCAAuC;iBACjD;aACF,CAAC,CAAC;QACL,CAAC;QACD,MAAM,OAAO,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;QAC5C,0EAA0E;QAC1E,0EAA0E;QAC1E,MAAM,IAAI,GAAG,uBAAuB,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACzD,2EAA2E;QAC3E,gEAAgE;QAChE,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;QAChD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE;YACzD,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACzC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACpC,CAAC,CAAC;QACH,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,27 @@
1
+ import { type GtmStorefrontConfig } from '@endora-commerce/contracts';
2
+ import type { SettingsReadPort } from '@endora-commerce/platform/kernel';
3
+ /**
4
+ * Resolves the per-sales-channel Google Tag Manager configuration exposed to
5
+ * the storefront.
6
+ *
7
+ * A blank or malformed container ID means the channel is untracked (FR-004 /
8
+ * FR-005): the returned config is fully disabled regardless of the master
9
+ * switch, so a typo can never reach a `<script src>`. `requireConsent` survives
10
+ * every disabled branch, because the storefront's shared consent gate reasons
11
+ * about the channel even when this module contributes nothing.
12
+ *
13
+ * Every setting read degrades to its documented default rather than throwing —
14
+ * an incomplete settings catalogue must not be able to fail a storefront
15
+ * render.
16
+ *
17
+ * The server container address is deliberately absent from the result: the
18
+ * browser only needs to know *that* it should relay, never *where* to.
19
+ */
20
+ export declare class GtmConfigService {
21
+ private readonly settings;
22
+ constructor(settings: SettingsReadPort);
23
+ getConfig(salesChannelId: string): Promise<GtmStorefrontConfig>;
24
+ private readBool;
25
+ private readString;
26
+ }
27
+ //# sourceMappingURL=gtm-config.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gtm-config.service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/gtm-config.service.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,mBAAmB,EACzB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAEzE;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,gBAAgB;IACf,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,gBAAgB;IAEjD,SAAS,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;YAmCvD,QAAQ;YAYR,UAAU;CAWzB"}
@@ -0,0 +1,65 @@
1
+ import { z } from 'zod';
2
+ import { GOOGLE_TAG_MANAGER_SETTING_CODES, GTM_DISABLED_CONFIG, gtmContainerIdSchema, } from '@endora-commerce/contracts';
3
+ /**
4
+ * Resolves the per-sales-channel Google Tag Manager configuration exposed to
5
+ * the storefront.
6
+ *
7
+ * A blank or malformed container ID means the channel is untracked (FR-004 /
8
+ * FR-005): the returned config is fully disabled regardless of the master
9
+ * switch, so a typo can never reach a `<script src>`. `requireConsent` survives
10
+ * every disabled branch, because the storefront's shared consent gate reasons
11
+ * about the channel even when this module contributes nothing.
12
+ *
13
+ * Every setting read degrades to its documented default rather than throwing —
14
+ * an incomplete settings catalogue must not be able to fail a storefront
15
+ * render.
16
+ *
17
+ * The server container address is deliberately absent from the result: the
18
+ * browser only needs to know *that* it should relay, never *where* to.
19
+ */
20
+ export class GtmConfigService {
21
+ settings;
22
+ constructor(settings) {
23
+ this.settings = settings;
24
+ }
25
+ async getConfig(salesChannelId) {
26
+ const [enabled, containerRaw, requireConsent] = await Promise.all([
27
+ this.readBool(GOOGLE_TAG_MANAGER_SETTING_CODES.ENABLED, salesChannelId, false),
28
+ this.readString(GOOGLE_TAG_MANAGER_SETTING_CODES.CONTAINER_ID, salesChannelId, ''),
29
+ this.readBool(GOOGLE_TAG_MANAGER_SETTING_CODES.REQUIRE_CONSENT, salesChannelId, true),
30
+ ]);
31
+ const containerId = gtmContainerIdSchema.safeParse(containerRaw.trim());
32
+ if (!enabled || !containerId.success) {
33
+ return { ...GTM_DISABLED_CONFIG, requireConsent };
34
+ }
35
+ const [serverSideEnabled, serverContainerUrl] = await Promise.all([
36
+ this.readBool(GOOGLE_TAG_MANAGER_SETTING_CODES.SERVER_SIDE_ENABLED, salesChannelId, false),
37
+ this.readString(GOOGLE_TAG_MANAGER_SETTING_CODES.SERVER_CONTAINER_URL, salesChannelId, ''),
38
+ ]);
39
+ return {
40
+ enabled: true,
41
+ containerId: containerId.data,
42
+ requireConsent,
43
+ // Server-side tagging with no destination is inert: the channel stays on
44
+ // the browser path rather than losing events (FR-031).
45
+ serverSide: serverSideEnabled && serverContainerUrl.trim() !== '',
46
+ };
47
+ }
48
+ async readBool(code, salesChannelId, fallback) {
49
+ try {
50
+ return await this.settings.get(code, salesChannelId, z.boolean());
51
+ }
52
+ catch {
53
+ return fallback;
54
+ }
55
+ }
56
+ async readString(code, salesChannelId, fallback) {
57
+ try {
58
+ return await this.settings.get(code, salesChannelId, z.string());
59
+ }
60
+ catch {
61
+ return fallback;
62
+ }
63
+ }
64
+ }
65
+ //# sourceMappingURL=gtm-config.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gtm-config.service.js","sourceRoot":"","sources":["../../../src/backend/services/gtm-config.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,gCAAgC,EAChC,mBAAmB,EACnB,oBAAoB,GAErB,MAAM,4BAA4B,CAAC;AAGpC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,OAAO,gBAAgB;IACE;IAA7B,YAA6B,QAA0B;QAA1B,aAAQ,GAAR,QAAQ,CAAkB;IAAG,CAAC;IAE3D,KAAK,CAAC,SAAS,CAAC,cAAsB;QACpC,MAAM,CAAC,OAAO,EAAE,YAAY,EAAE,cAAc,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YAChE,IAAI,CAAC,QAAQ,CAAC,gCAAgC,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,CAAC;YAC9E,IAAI,CAAC,UAAU,CAAC,gCAAgC,CAAC,YAAY,EAAE,cAAc,EAAE,EAAE,CAAC;YAClF,IAAI,CAAC,QAAQ,CAAC,gCAAgC,CAAC,eAAe,EAAE,cAAc,EAAE,IAAI,CAAC;SACtF,CAAC,CAAC;QAEH,MAAM,WAAW,GAAG,oBAAoB,CAAC,SAAS,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC;QACxE,IAAI,CAAC,OAAO,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC;YACrC,OAAO,EAAE,GAAG,mBAAmB,EAAE,cAAc,EAAE,CAAC;QACpD,CAAC;QAED,MAAM,CAAC,iBAAiB,EAAE,kBAAkB,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YAChE,IAAI,CAAC,QAAQ,CACX,gCAAgC,CAAC,mBAAmB,EACpD,cAAc,EACd,KAAK,CACN;YACD,IAAI,CAAC,UAAU,CACb,gCAAgC,CAAC,oBAAoB,EACrD,cAAc,EACd,EAAE,CACH;SACF,CAAC,CAAC;QAEH,OAAO;YACL,OAAO,EAAE,IAAI;YACb,WAAW,EAAE,WAAW,CAAC,IAAI;YAC7B,cAAc;YACd,yEAAyE;YACzE,uDAAuD;YACvD,UAAU,EAAE,iBAAiB,IAAI,kBAAkB,CAAC,IAAI,EAAE,KAAK,EAAE;SAClE,CAAC;IACJ,CAAC;IAEO,KAAK,CAAC,QAAQ,CACpB,IAAY,EACZ,cAAsB,EACtB,QAAiB;QAEjB,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QACpE,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,QAAQ,CAAC;QAClB,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,UAAU,CACtB,IAAY,EACZ,cAAsB,EACtB,QAAgB;QAEhB,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;QACnE,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,QAAQ,CAAC;QAClB,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Client for the operator's server-side Google Tag Manager container (feature
3
+ * 066, US3).
4
+ *
5
+ * Posts one event to `{server_container_url}{server_ingest_path}` as a JSON
6
+ * envelope the container's ingest client parses — Google's Data Client (default
7
+ * path `/data`) or an equivalent custom client. No credentials are sent: an
8
+ * sGTM ingest endpoint is a public collection endpoint by design, exactly like
9
+ * `google-analytics.com/g/collect`.
10
+ *
11
+ * Like `Ga4MpClient` and unlike the legacy fire-and-forget forwarder, this
12
+ * client THROWS on a non-2xx response or a transport error so the BullMQ worker
13
+ * retries (Principle X / FR-027). `fetchFn` is injectable, so no test touches
14
+ * the network.
15
+ */
16
+ import type { GtmPageContext } from '@endora-commerce/contracts';
17
+ export interface SgtmDestination {
18
+ /** Base URL of the server container, e.g. `https://sgtm.example.com`. */
19
+ baseUrl: string;
20
+ /** Request path the container's ingest client claims. Blank ⇒ `/data`. */
21
+ ingestPath: string;
22
+ }
23
+ export interface SgtmEvent {
24
+ eventId: string;
25
+ clientId: string;
26
+ name: string;
27
+ params: Record<string, string | number | boolean>;
28
+ consent: {
29
+ analyticsStorage: 'granted' | 'denied';
30
+ };
31
+ page: GtmPageContext;
32
+ /** Present only when the relay processor kept them (granted consent). */
33
+ ip?: string;
34
+ userAgent?: string;
35
+ }
36
+ export interface SgtmClientOptions {
37
+ fetchFn?: typeof fetch;
38
+ timeoutMs?: number;
39
+ }
40
+ export declare class SgtmClient {
41
+ private readonly options;
42
+ constructor(options?: SgtmClientOptions);
43
+ get timeoutMs(): number;
44
+ send(dest: SgtmDestination, event: SgtmEvent): Promise<void>;
45
+ }
46
+ //# sourceMappingURL=sgtm-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sgtm-client.d.ts","sourceRoot":"","sources":["../../../src/backend/services/sgtm-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC9B,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;IAClD,OAAO,EAAE;QAAE,gBAAgB,EAAE,SAAS,GAAG,QAAQ,CAAA;KAAE,CAAC;IACpD,IAAI,EAAE,cAAc,CAAC;IACrB,yEAAyE;IACzE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA4BD,qBAAa,UAAU;IACT,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,GAAE,iBAAsB;IAE5D,IAAI,SAAS,IAAI,MAAM,CAEtB;IAEK,IAAI,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;CA4CnE"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Client for the operator's server-side Google Tag Manager container (feature
3
+ * 066, US3).
4
+ *
5
+ * Posts one event to `{server_container_url}{server_ingest_path}` as a JSON
6
+ * envelope the container's ingest client parses — Google's Data Client (default
7
+ * path `/data`) or an equivalent custom client. No credentials are sent: an
8
+ * sGTM ingest endpoint is a public collection endpoint by design, exactly like
9
+ * `google-analytics.com/g/collect`.
10
+ *
11
+ * Like `Ga4MpClient` and unlike the legacy fire-and-forget forwarder, this
12
+ * client THROWS on a non-2xx response or a transport error so the BullMQ worker
13
+ * retries (Principle X / FR-027). `fetchFn` is injectable, so no test touches
14
+ * the network.
15
+ */
16
+ const DEFAULT_INGEST_PATH = '/data';
17
+ const DEFAULT_TIMEOUT_MS = 10_000;
18
+ export class SgtmClient {
19
+ options;
20
+ constructor(options = {}) {
21
+ this.options = options;
22
+ }
23
+ get timeoutMs() {
24
+ return this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
25
+ }
26
+ async send(dest, event) {
27
+ const fetchFn = this.options.fetchFn ?? globalThis.fetch;
28
+ const base = dest.baseUrl.trim().replace(/\/+$/, '');
29
+ const rawPath = dest.ingestPath.trim() || DEFAULT_INGEST_PATH;
30
+ const path = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
31
+ const granted = event.consent.analyticsStorage === 'granted';
32
+ const envelope = {
33
+ event_name: event.name,
34
+ client_id: event.clientId,
35
+ event_id: event.eventId,
36
+ page_location: event.page.location,
37
+ ...(event.page.referrer ? { page_referrer: event.page.referrer } : {}),
38
+ ...(event.page.title ? { page_title: event.page.title } : {}),
39
+ ...(event.page.language ? { language: event.page.language } : {}),
40
+ consent: { analytics_storage: event.consent.analyticsStorage },
41
+ // Second enforcement point for FR-030: the processor already strips them
42
+ // on denied consent, and the last gate before the wire refuses to send
43
+ // them regardless of what reached it.
44
+ ...(granted && event.ip ? { ip_override: event.ip } : {}),
45
+ ...(granted && event.userAgent ? { user_agent: event.userAgent } : {}),
46
+ };
47
+ // Params are flattened at the top level, but the envelope wins on a
48
+ // collision — a caller-supplied `event_name` must not be able to redirect
49
+ // the operator's tags.
50
+ const body = { ...event.params, ...envelope };
51
+ const controller = new AbortController();
52
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
53
+ try {
54
+ const res = await fetchFn(`${base}${path}`, {
55
+ method: 'POST',
56
+ headers: { 'Content-Type': 'application/json' },
57
+ signal: controller.signal,
58
+ body: JSON.stringify(body),
59
+ });
60
+ if (!res.ok) {
61
+ throw new Error(`sGTM delivery failed with status ${res.status}`);
62
+ }
63
+ }
64
+ finally {
65
+ clearTimeout(timeout);
66
+ }
67
+ }
68
+ }
69
+ //# sourceMappingURL=sgtm-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sgtm-client.js","sourceRoot":"","sources":["../../../src/backend/services/sgtm-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import { Queue, Worker, type Processor, type QueueOptions, type WorkerOptions } from 'bullmq';
2
+ import type { Redis } from 'ioredis';
3
+ import type { GtmCollectRequest, GtmPageContext } from '@endora-commerce/contracts';
4
+ /**
5
+ * BullMQ queue for the server-side Google Tag Manager relay (feature 066, US3 /
6
+ * Principle X). The storefront `/collect` route is a pure producer: it
7
+ * validates and enqueues one job per event, and the worker forwards each to the
8
+ * operator's server container, throwing on failure so BullMQ retries.
9
+ *
10
+ * Nothing here is persisted in PostgreSQL — the relay is stateless, and
11
+ * `removeOnFail` retention is what makes an exhausted delivery observable
12
+ * (FR-027).
13
+ */
14
+ export interface GtmRelayJobData {
15
+ /** Also the BullMQ `jobId` — the idempotency key across retries (FR-028). */
16
+ eventId: string;
17
+ salesChannelId: string;
18
+ clientId: string;
19
+ event: {
20
+ name: string;
21
+ params: Record<string, string | number | boolean>;
22
+ };
23
+ consent: {
24
+ analyticsStorage: 'granted' | 'denied';
25
+ };
26
+ page: GtmPageContext;
27
+ /** Captured server-side at ingest; forwarded only with granted consent (FR-030). */
28
+ ip?: string;
29
+ userAgent?: string;
30
+ /** Stamped by the producer, so retry latency does not distort the timing. */
31
+ occurredAt: string;
32
+ }
33
+ /** Server-observed request context the browser is never trusted to supply. */
34
+ export interface GtmIngestContext {
35
+ ip?: string;
36
+ userAgent?: string;
37
+ }
38
+ /**
39
+ * Producer callback exposed to the storefront route. Assigns an `eventId` per
40
+ * event, enqueues a relay job for each, and returns how many were accepted.
41
+ */
42
+ export type GtmCollectEnqueuer = (salesChannelId: string, request: GtmCollectRequest, context: GtmIngestContext) => Promise<number>;
43
+ export declare const GTM_RELAY_QUEUE_NAME = "google_tag_manager.ss.relay";
44
+ export declare function createGtmRelayQueue(redis: Redis, overrides?: Partial<QueueOptions>): Queue<GtmRelayJobData>;
45
+ export declare function createGtmRelayWorker(redis: Redis, processor: Processor<GtmRelayJobData>, overrides?: Partial<WorkerOptions>): Worker<GtmRelayJobData>;
46
+ //# sourceMappingURL=ss-relay-queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ss-relay-queue.d.ts","sourceRoot":"","sources":["../../../src/backend/services/ss-relay-queue.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,SAAS,EAAE,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,QAAQ,CAAC;AAE9F,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEpF;;;;;;;;;GASG;AACH,MAAM,WAAW,eAAe;IAC9B,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAA;KAAE,CAAC;IAC3E,OAAO,EAAE;QAAE,gBAAgB,EAAE,SAAS,GAAG,QAAQ,CAAA;KAAE,CAAC;IACpD,IAAI,EAAE,cAAc,CAAC;IACrB,oFAAoF;IACpF,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,8EAA8E;AAC9E,MAAM,WAAW,gBAAgB;IAC/B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAC/B,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,gBAAgB,KACtB,OAAO,CAAC,MAAM,CAAC,CAAC;AAErB,eAAO,MAAM,oBAAoB,gCAAgC,CAAC;AAKlE,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,KAAK,EACZ,SAAS,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAChC,KAAK,CAAC,eAAe,CAAC,CAYxB;AAED,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,KAAK,EACZ,SAAS,EAAE,SAAS,CAAC,eAAe,CAAC,EACrC,SAAS,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,GACjC,MAAM,CAAC,eAAe,CAAC,CAazB"}
@@ -0,0 +1,29 @@
1
+ import { Queue, Worker } from 'bullmq';
2
+ import { enterSystemScope } from '@endora-commerce/platform/kernel';
3
+ export const GTM_RELAY_QUEUE_NAME = 'google_tag_manager.ss.relay';
4
+ const DEFAULT_ATTEMPTS = 8;
5
+ const DEFAULT_BACKOFF = { type: 'exponential', delay: 1_000 };
6
+ export function createGtmRelayQueue(redis, overrides) {
7
+ const options = {
8
+ connection: redis,
9
+ defaultJobOptions: {
10
+ attempts: DEFAULT_ATTEMPTS,
11
+ backoff: DEFAULT_BACKOFF,
12
+ removeOnComplete: { count: 500 },
13
+ removeOnFail: { count: 5_000 },
14
+ },
15
+ ...overrides,
16
+ };
17
+ return new Queue(GTM_RELAY_QUEUE_NAME, options);
18
+ }
19
+ export function createGtmRelayWorker(redis, processor, overrides) {
20
+ const options = {
21
+ connection: redis,
22
+ concurrency: 8,
23
+ ...overrides,
24
+ };
25
+ // Feature 072 (T033) — the job establishes its own scope; it established
26
+ // nothing before.
27
+ return new Worker(GTM_RELAY_QUEUE_NAME, (job) => enterSystemScope('google_tag_manager: server-side relay', () => processor(job)), options);
28
+ }
29
+ //# sourceMappingURL=ss-relay-queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ss-relay-queue.js","sourceRoot":"","sources":["../../../src/backend/services/ss-relay-queue.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAyD,MAAM,QAAQ,CAAC;AAC9F,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AA6CpE,MAAM,CAAC,MAAM,oBAAoB,GAAG,6BAA6B,CAAC;AAElE,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAC3B,MAAM,eAAe,GAAG,EAAE,IAAI,EAAE,aAAsB,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAEvE,MAAM,UAAU,mBAAmB,CACjC,KAAY,EACZ,SAAiC;IAEjC,MAAM,OAAO,GAAiB;QAC5B,UAAU,EAAE,KAAK;QACjB,iBAAiB,EAAE;YACjB,QAAQ,EAAE,gBAAgB;YAC1B,OAAO,EAAE,eAAe;YACxB,gBAAgB,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE;YAChC,YAAY,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE;SAC/B;QACD,GAAG,SAAS;KACb,CAAC;IACF,OAAO,IAAI,KAAK,CAAkB,oBAAoB,EAAE,OAAO,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,KAAY,EACZ,SAAqC,EACrC,SAAkC;IAElC,MAAM,OAAO,GAAkB;QAC7B,UAAU,EAAE,KAAK;QACjB,WAAW,EAAE,CAAC;QACd,GAAG,SAAS;KACb,CAAC;IACF,yEAAyE;IACzE,kBAAkB;IAClB,OAAO,IAAI,MAAM,CACf,oBAAoB,EACpB,CAAC,GAAG,EAAE,EAAE,CAAC,gBAAgB,CAAC,uCAAuC,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,EACxF,OAAO,CACR,CAAC;AACJ,CAAC"}
@@ -0,0 +1,24 @@
1
+ import type { Processor, Queue } from 'bullmq';
2
+ import { type GtmCollectRequest } from '@endora-commerce/contracts';
3
+ import type { SettingsReadPort } from '@endora-commerce/platform/kernel';
4
+ import type { GtmIngestContext, GtmRelayJobData } from './ss-relay-queue.js';
5
+ import type { SgtmClient } from './sgtm-client.js';
6
+ /**
7
+ * Server-side relay orchestration (feature 066, US3).
8
+ *
9
+ * - `makeEnqueuer` returns the producer used by the `/collect` route: it
10
+ * assigns one uuid `eventId` per event (the idempotency key and the BullMQ
11
+ * `jobId`) and enqueues one relay job each. Pure producer — no outbound call
12
+ * happens in the shopper's request (Principle X / FR-029).
13
+ * - `makeProcessor` returns the BullMQ processor: it resolves the channel's
14
+ * server container address from Settings, drops the visitor's IP and user
15
+ * agent unless consent was granted (FR-030), and forwards through the sGTM
16
+ * client, letting a failure throw so BullMQ retries.
17
+ */
18
+ export declare function makeEnqueuer(queue: Queue<GtmRelayJobData>): (salesChannelId: string, request: GtmCollectRequest, context: GtmIngestContext) => Promise<number>;
19
+ export interface GtmRelayProcessorDeps {
20
+ settings: SettingsReadPort;
21
+ client: SgtmClient;
22
+ }
23
+ export declare function makeProcessor(deps: GtmRelayProcessorDeps): Processor<GtmRelayJobData>;
24
+ //# sourceMappingURL=ss-relay.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ss-relay.service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/ss-relay.service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAC/C,OAAO,EAAoC,KAAK,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AACtG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACzE,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC7E,OAAO,KAAK,EAAE,UAAU,EAAa,MAAM,kBAAkB,CAAC;AAE9D;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,eAAe,CAAC,IAEtD,gBAAgB,MAAM,EACtB,SAAS,iBAAiB,EAC1B,SAAS,gBAAgB,KACxB,OAAO,CAAC,MAAM,CAAC,CA4BnB;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,MAAM,EAAE,UAAU,CAAC;CACpB;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,qBAAqB,GAAG,SAAS,CAAC,eAAe,CAAC,CAmCrF"}