@owlmeans/web-marketing-consent 0.1.18-rc.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 (125) hide show
  1. package/README.md +15 -0
  2. package/agent-meta/manifest.json +16 -0
  3. package/agent-meta/skills/web-marketing-consent/SKILL.md +245 -0
  4. package/build/@/components/ui/button.d.ts +11 -0
  5. package/build/@/components/ui/button.d.ts.map +1 -0
  6. package/build/@/components/ui/button.js +32 -0
  7. package/build/@/components/ui/button.js.map +1 -0
  8. package/build/@/components/ui/card.d.ts +10 -0
  9. package/build/@/components/ui/card.d.ts.map +1 -0
  10. package/build/@/components/ui/card.js +25 -0
  11. package/build/@/components/ui/card.js.map +1 -0
  12. package/build/@/components/ui/label.d.ts +5 -0
  13. package/build/@/components/ui/label.d.ts.map +1 -0
  14. package/build/@/components/ui/label.js +8 -0
  15. package/build/@/components/ui/label.js.map +1 -0
  16. package/build/@/lib/utils.d.ts +3 -0
  17. package/build/@/lib/utils.d.ts.map +1 -0
  18. package/build/@/lib/utils.js +6 -0
  19. package/build/@/lib/utils.js.map +1 -0
  20. package/build/append.d.ts +32 -0
  21. package/build/append.d.ts.map +1 -0
  22. package/build/append.js +28 -0
  23. package/build/append.js.map +1 -0
  24. package/build/bridge.d.ts +25 -0
  25. package/build/bridge.d.ts.map +1 -0
  26. package/build/bridge.js +69 -0
  27. package/build/bridge.js.map +1 -0
  28. package/build/components/fields.d.ts +18 -0
  29. package/build/components/fields.d.ts.map +1 -0
  30. package/build/components/fields.js +22 -0
  31. package/build/components/fields.js.map +1 -0
  32. package/build/components/index.d.ts +3 -0
  33. package/build/components/index.d.ts.map +1 -0
  34. package/build/components/index.js +3 -0
  35. package/build/components/index.js.map +1 -0
  36. package/build/components/preferences.d.ts +19 -0
  37. package/build/components/preferences.d.ts.map +1 -0
  38. package/build/components/preferences.js +33 -0
  39. package/build/components/preferences.js.map +1 -0
  40. package/build/components/screen.d.ts +11 -0
  41. package/build/components/screen.d.ts.map +1 -0
  42. package/build/components/screen.js +35 -0
  43. package/build/components/screen.js.map +1 -0
  44. package/build/consts.d.ts +26 -0
  45. package/build/consts.d.ts.map +1 -0
  46. package/build/consts.js +27 -0
  47. package/build/consts.js.map +1 -0
  48. package/build/entrypoints.d.ts +12 -0
  49. package/build/entrypoints.d.ts.map +1 -0
  50. package/build/entrypoints.js +16 -0
  51. package/build/entrypoints.js.map +1 -0
  52. package/build/hooks/use-marketing-consent.d.ts +38 -0
  53. package/build/hooks/use-marketing-consent.d.ts.map +1 -0
  54. package/build/hooks/use-marketing-consent.js +96 -0
  55. package/build/hooks/use-marketing-consent.js.map +1 -0
  56. package/build/i18n/be.json +19 -0
  57. package/build/i18n/de.json +19 -0
  58. package/build/i18n/en.json +19 -0
  59. package/build/i18n/es.json +19 -0
  60. package/build/i18n/fr.json +19 -0
  61. package/build/i18n/pl.json +19 -0
  62. package/build/i18n/ru.json +19 -0
  63. package/build/i18n/uk.json +19 -0
  64. package/build/i18n.d.ts +2 -0
  65. package/build/i18n.d.ts.map +1 -0
  66. package/build/i18n.js +25 -0
  67. package/build/i18n.js.map +1 -0
  68. package/build/index.d.ts +11 -0
  69. package/build/index.d.ts.map +1 -0
  70. package/build/index.js +11 -0
  71. package/build/index.js.map +1 -0
  72. package/build/landing.d.ts +22 -0
  73. package/build/landing.d.ts.map +1 -0
  74. package/build/landing.js +68 -0
  75. package/build/landing.js.map +1 -0
  76. package/build/service.d.ts +40 -0
  77. package/build/service.d.ts.map +1 -0
  78. package/build/service.js +80 -0
  79. package/build/service.js.map +1 -0
  80. package/build/step.d.ts +10 -0
  81. package/build/step.d.ts.map +1 -0
  82. package/build/step.js +16 -0
  83. package/build/step.js.map +1 -0
  84. package/components.json +21 -0
  85. package/package.json +77 -0
  86. package/src/@/components/ui/button.tsx +60 -0
  87. package/src/@/components/ui/card.tsx +93 -0
  88. package/src/@/components/ui/label.tsx +23 -0
  89. package/src/@/globals.css +57 -0
  90. package/src/@/lib/utils.ts +6 -0
  91. package/src/append.ts +62 -0
  92. package/src/bridge.ts +89 -0
  93. package/src/components/fields.tsx +101 -0
  94. package/src/components/index.ts +2 -0
  95. package/src/components/preferences.tsx +69 -0
  96. package/src/components/screen.tsx +90 -0
  97. package/src/consts.ts +31 -0
  98. package/src/entrypoints.ts +17 -0
  99. package/src/hooks/use-marketing-consent.ts +150 -0
  100. package/src/i18n/be.json +19 -0
  101. package/src/i18n/de.json +19 -0
  102. package/src/i18n/en.json +19 -0
  103. package/src/i18n/es.json +19 -0
  104. package/src/i18n/fr.json +19 -0
  105. package/src/i18n/pl.json +19 -0
  106. package/src/i18n/ru.json +19 -0
  107. package/src/i18n/uk.json +19 -0
  108. package/src/i18n.ts +26 -0
  109. package/src/index.ts +10 -0
  110. package/src/landing.ts +80 -0
  111. package/src/service.ts +131 -0
  112. package/src/step.ts +21 -0
  113. package/tests/bridge.spec.ts +100 -0
  114. package/tests/context.ts +48 -0
  115. package/tests/harness/frame.html +16 -0
  116. package/tests/harness/index.html +11 -0
  117. package/tests/harness/mount.tsx +108 -0
  118. package/tests/helpers.ts +88 -0
  119. package/tests/i18n.spec.ts +63 -0
  120. package/tests/package-boundary.spec.ts +19 -0
  121. package/tests/preferences.spec.ts +70 -0
  122. package/tests/screen.spec.ts +146 -0
  123. package/tests/service.spec.ts +80 -0
  124. package/tests/tsconfig.json +12 -0
  125. package/tsconfig.json +14 -0
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ <!-- owlmeans:agent-guidance:start -->
2
+ ## Agent guidance
3
+
4
+ This package ships embedded agent skills under `agent-meta/`. After installing your
5
+ `@owlmeans/*` packages, run the OwlMeans agent-skills installer to place them into
6
+ your project's skill store (`.agents/skills/`):
7
+
8
+ ```sh
9
+ npx @owlmeans/agent-skills@^0.1.18-rc.33
10
+ ```
11
+
12
+ The embedded files are version-matched to this package release. Do not edit them
13
+ directly — they are regenerated on each publish. To contribute guidance edits,
14
+ open a PR against the source monorepo.
15
+ <!-- owlmeans:agent-guidance:end -->
@@ -0,0 +1,16 @@
1
+ {
2
+ "schemaVersion": 2,
3
+ "package": "@owlmeans/web-marketing-consent",
4
+ "version": "0.1.18-rc.0",
5
+ "generatedAt": "2026-09-22T17:37:10.922Z",
6
+ "canonicalRepo": "https://github.com/owlmeans/common",
7
+ "entries": [
8
+ {
9
+ "kind": "skill",
10
+ "name": "web-marketing-consent",
11
+ "category": "package-specific",
12
+ "file": "skills/web-marketing-consent/SKILL.md",
13
+ "canonicalPath": ".agents/skills/web-marketing-consent/SKILL.md"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,245 @@
1
+ ---
2
+ name: web-marketing-consent
3
+ description: How to use @owlmeans/web-marketing-consent — the browser half of @owlmeans/marketing-consent: the post-sign-in privacy-choices screen, a reusable MarketingConsentPreferences settings-card body, appendMarketingConsent's login step and landing hooks, the MarketingConsentClientService (fail-open, signed-out-safe), and the cookie-consent bridge to @owlmeans/consent. Auto-invoked when wiring the marketing-consent screen into a web app, rendering a settings card for privacy choices, reconciling cookie-consent decisions with saved account decisions, or diagnosing a person stuck on (or never shown) the marketing-consent step.
4
+ user-invocable: false
5
+ ---
6
+ <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
7
+
8
+ # @owlmeans/web-marketing-consent
9
+
10
+ **Layer:** Web (React, shadcn + Tailwind v4)
11
+ **Install:** `"@owlmeans/web-marketing-consent": "^0.1.18-rc.0"` in `dependencies`
12
+ **Contracts:** `@owlmeans/marketing-consent` — the catalogue, `consentStatus`, the protocol tree
13
+ **Server half:** `@owlmeans/server-marketing-consent` — `serveMarketingConsentEntrypoints` binds the
14
+ same `status`/`save`/`terms` protocols this package calls
15
+ **Login seam:** `@owlmeans/client-auth`'s `LoginStep`/`LoginLandingHook` registries (`./login`)
16
+ **Cookie bridge:** `@owlmeans/consent`'s `consentStore` (device-side cookie/tracking decisions)
17
+
18
+ Read `@owlmeans/marketing-consent`'s own skill first (the catalogue, `consentStatus`'s decision
19
+ table, the protocol tree) and `@owlmeans/server-marketing-consent`'s (the service, `subjectOf`, the
20
+ append-only log) — this package builds on both and repeats nothing they already document.
21
+
22
+ ## Key exports
23
+
24
+ | Export | Description |
25
+ |---|---|
26
+ | `MARKETING_CONSENT_CLIENT_SERVICE` | The client service alias (`context.service<MarketingConsentClientService>(...)`) |
27
+ | `MARKETING_CONSENT_LOGIN_STEP` · `MARKETING_CONSENT_LANDING_HOOK_TERMS` · `MARKETING_CONSENT_LANDING_HOOK_SYNC` | Registry aliases the step/hooks register under |
28
+ | `MARKETING_CONSENT_I18N` | Re-export of `@owlmeans/marketing-consent`'s OWN `MARKETING_CONSENT_I18N` — see "One shared i18n resource" below |
29
+ | `makeMarketingConsentClient(protocols, opts?)` · `appendMarketingConsentClient(ctx, protocols, opts?)` | Build/register the `MarketingConsentClientService` |
30
+ | `MarketingConsentClientService` | `status(opts?)`, `save(request)`, `recordTerms(acceptance)`, `last()`, `bridges()`, `preferences()` — see "Fail open, always" |
31
+ | `cookieConsentBridge(opts?)` | A `MarketingConsentBridge` over `@owlmeans/consent`'s `consentStore` |
32
+ | `marketingConsentStep(client, entrypointAlias)` | The `LoginStep` — pending while `status().pending` is true |
33
+ | `termsRecorder(loginTermsConfig, client, locale?)` · `landingSync(client)` | The two `LoginLandingHook`s |
34
+ | `appendMarketingConsent(ctx, opts)` | The ONE call an app makes — wires the client, the step and both landing hooks |
35
+ | `marketingConsentEntrypoints(protocols)` | `[bindScreen(protocols.screen, handler(MarketingConsentScreen))]` |
36
+ | `MarketingConsentScreen` | The post-sign-in screen (`RoutedComponent`) |
37
+ | `MarketingConsentPreferences` | The reusable settings-card body (`{ translate?, className?, onSaved? }`) |
38
+ | `useMarketingConsent(opts?)` | The headless model both components share — `{ loading, saving, error, gpc, groups, allChecked, allIndeterminate, toggleAll, toggle, save, skip }` |
39
+
40
+ ## Wiring
41
+
42
+ ```ts
43
+ import { appendMarketingConsent, marketingConsentEntrypoints } from '@owlmeans/web-marketing-consent'
44
+ import { makeMarketingConsentProtocols } from '@owlmeans/marketing-consent'
45
+
46
+ // shared package — the SAME object the server binds `serveMarketingConsentEntrypoints` to
47
+ export const marketingConsentProtocols = makeMarketingConsentProtocols({ parent: appProtocols.account.base })
48
+
49
+ // web app's makeContext
50
+ appendMarketingConsent(context, { protocols: marketingConsentProtocols })
51
+ // entrypoints: ...marketingConsentEntrypoints(marketingConsentProtocols)
52
+ // PLUS the app's own bindAll for base/status/save/terms, alongside the server's bindAll — this
53
+ // package binds only the SCREEN. Exactly the split @owlmeans/web-oauth's oauthEntrypoints draws
54
+ // for its own consent API; see that package's skill if this split is unfamiliar.
55
+ ```
56
+
57
+ `appendMarketingConsent(ctx, opts)` does four things, each independently toggleable:
58
+
59
+ 1. Registers the `MarketingConsentClientService` (`opts.bridges ?? [cookieConsentBridge()]`,
60
+ `opts.preferences`) — via `appendMarketingConsentClient`, so calling `appendMarketingConsent`
61
+ twice never double-registers.
62
+ 2. `opts.step !== false` — `login.registerStep(marketingConsentStep(client, opts.protocols.screen.alias))`.
63
+ 3. `opts.terms !== false` — reads `(context.cfg as CommonConfig).security?.auth?.login?.terms`
64
+ (the SAME typed path `@owlmeans/client-panel`'s `useLoginMethods` reads) and registers
65
+ `termsRecorder`.
66
+ 4. Always registers `landingSync`.
67
+
68
+ `opts.config` (`MarketingConsentConfig`) is accepted for symmetry with the server's
69
+ `appendMarketingConsentService({ config })` call but is NOT read here — `resolveMarketingConsents`
70
+ only ever runs where the catalogue is enumerated (the server); this package's UI reads the
71
+ ALREADY-RESOLVED `MarketingConsentStatusItem.definition` back off the status response and never
72
+ re-resolves the catalogue client-side.
73
+
74
+ ## One shared i18n resource, not two
75
+
76
+ `MARKETING_CONSENT_I18N` here is a re-export of `@owlmeans/marketing-consent`'s OWN constant
77
+ (`'marketing-consent'`) — this package does NOT register a second resource for its `screen.*`/
78
+ `preferences.*` strings. `@owlmeans/i18n`'s `addI18nLib` pushes a new resource ENTRY rather than
79
+ replacing one, and `client-i18n`'s `useI18nResource` merges every entry for a given
80
+ `(lng, resource, ns)` with `i18n.addResourceBundle(lng, ns, data, true, true)` (deep merge) before
81
+ it is ever read. The domain package's bundle (`group.*`, `consent.*`, `link.*`, `errors.*`) and
82
+ this package's own (`screen.*`, `preferences.*`) have disjoint top-level keys, so the two combine
83
+ cleanly. `ConsentFields` (the shared group/item body) reads BOTH halves through one `t` —
84
+ `t(item.definition.labelKey, ...)` for a domain key, `t('screen.updated', ...)` for one of ours,
85
+ `t('link.default', 'Learn more')` for the domain package's own link fallback text.
86
+
87
+ ## Fail open, always
88
+
89
+ `MarketingConsentClientService` NEVER calls the API while signed out
90
+ (`AuthService.authenticated()` — no args, returns the token or `null` — checked before every call)
91
+ and NEVER throws into a caller: any rejected/thrown network call is swallowed, returning `null`
92
+ (`status`, `save`) or `false` (`recordTerms`) instead. A broken consent read/write must never block
93
+ sign-in or break the app — the same discipline `@owlmeans/web-oauth`'s `landAfterLogin`/
94
+ `continueLogin` already apply to a step/hook that hangs or throws (bound-and-swallow, timeout
95
+ included).
96
+
97
+ `status(opts?)` also caches: `opts.fresh !== true` and an already-cached `last()` skips the network
98
+ round trip entirely (a footer "Privacy choices" link wants a quick paint, not a forced refetch);
99
+ `marketingConsentStep`/`landingSync`/the two components all pass `{ fresh: true }` because a
100
+ post-sign-in decision must never be stale.
101
+
102
+ ## The login step and the two landing hooks
103
+
104
+ `marketingConsentStep(client, entrypointAlias)` is `pending: async () => (await client.status({
105
+ fresh: true }))?.pending ?? false` — a fetch failure fails OPEN (not pending), because a broken
106
+ read must never permanently trap a signed-in person on a screen they cannot get past.
107
+ `entrypointAlias` is `protocols.screen.alias` — `continueLogin`'s `LoginStep.entrypoint` is where
108
+ the dispatcher sends the browser while the step is pending.
109
+
110
+ `termsRecorder` and `landingSync` are `LoginLandingHook`s, run once per freshly landed token by
111
+ `landAfterLogin` (see `@owlmeans/client-auth`'s `./login` skill for the bound-and-swallow contract
112
+ they share with every other landing hook):
113
+
114
+ - **`termsRecorder`** (priority 100, runs first) — if `resolveTerms(loginTermsConfig)` is required
115
+ and `termsAccepted(resolved)` (the LOCAL `localStorage` marker `@owlmeans/client-auth`'s sign-in
116
+ screen already set before the flow was allowed to start), posts `client.recordTerms({ documents,
117
+ notices, version, locale })`. A config with terms disabled, or a person who has not yet locally
118
+ accepted, records nothing.
119
+ - **`landingSync`** (priority 90) — reconciles every `client.bridges()` against the freshly loaded
120
+ status: a device with cookie decisions but no saved account decisions SEEDS the account
121
+ (`client.save({ source: 'cookie' })`); an account with decisions but a bare device SEEDS the
122
+ device (`bridge.write`); both present (agreeing or not), or neither present, and NOTHING is
123
+ overwritten — the safer of two wrong guesses is to leave two already-made choices alone rather
124
+ than silently pick one.
125
+
126
+ `MarketingConsentScreen`'s own "Save and continue"/"Continue without saving" buttons call
127
+ `useContinueLogin()({ after: MARKETING_CONSENT_LOGIN_STEP })` directly — that is `continueLogin`,
128
+ NOT `landAfterLogin`, so clicking through this step never re-runs `termsRecorder`/`landingSync`
129
+ (those only fire once, on the token that just landed, before this step's screen was ever reached).
130
+
131
+ ## The cookie-consent bridge
132
+
133
+ `cookieConsentBridge(opts?: { storageOptions?: ConsentOptions })` is a `MarketingConsentBridge`
134
+ over `@owlmeans/consent`'s singleton `consentStore`, for every `MarketingConsentDefinition` whose
135
+ `cookieCategory` names a `@owlmeans/consent` category (`trackers.analytics` -> `CONSENT_ANALYTICS`,
136
+ `trackers.advertising` -> `CONSENT_MARKETING`). `consentStore.save` REPLACES the stored record
137
+ wholesale (no merge of its own), so `write` always spreads the CURRENT record first and always
138
+ re-asserts the essential category before patching in only the categories this bridge owns.
139
+ `opts.storageOptions` is read ONLY to find which category is "essential" (a host that renamed it
140
+ via its own `consentStore.init(storageOptions)` elsewhere is still honored); this bridge never
141
+ calls `consentStore.init` itself — it has no opinion about whether a cookie dialog exists at all.
142
+
143
+ A `suppress` flag guards re-entrancy: `write` sets it before calling `consentStore.save` and clears
144
+ it in a `finally`, and `subscribe`'s own listener skips while it is set — otherwise a save made
145
+ THROUGH this bridge would immediately hand the same change back to whatever reacts to `subscribe`,
146
+ which could write again and loop. An update made OUTSIDE the bridge (the cookie dialog's own UI)
147
+ still reaches every subscriber normally.
148
+
149
+ ## The screen and the settings card
150
+
151
+ `MarketingConsentScreen` — full page, no application chrome, same `100dvh` centered-card shape as
152
+ `@owlmeans/web-oauth`'s consent screen. **Renders when framed too** — unlike that screen (a
153
+ token-mint decision a clickjacking overlay could steal), saving a marketing preference is not one,
154
+ so there is deliberately no framed-refusal branch. Nothing checked by default is a fully valid
155
+ save; there is no minimum-selection validation anywhere.
156
+
157
+ `MarketingConsentPreferences({ translate?, className?, onSaved? })` renders the SAME group/item/
158
+ select-all body (`ConsentFields`, internal — not exported) without the full-page chrome, for a
159
+ host's own settings card. `MarketingConsentClientService.preferences()` names the entrypoint alias
160
+ a footer "Privacy choices" link should point at; that screen is expected to render THIS component,
161
+ not `MarketingConsentScreen`. No `useContinueLogin` here at all — nothing navigates on save, and
162
+ there is no skip: a settings card is always revisitable, so failing to save just leaves the error
163
+ showing and the draft in place.
164
+
165
+ `useMarketingConsent({ source })` is the shared headless model: `source: 'sign-in'` from the screen,
166
+ `'settings'` (the default) from preferences — posted on every `save()` call, on EVERY item's
167
+ current draft value (never a diff; the server upserts by key). Groups are ordered
168
+ `communications`/`data`/`trackers` (`MC_GROUP_*`), any other group appended after in first-seen
169
+ order. `allIndeterminate` is applied to the select-all checkbox's `.indeterminate` DOM property via
170
+ a `ref` in a `useEffect` — a native checkbox has no ARIA `mixed` prop, only that imperative one.
171
+
172
+ ## `data-marketing-consent-*` contract
173
+
174
+ `data-marketing-consent` (screen root), `data-marketing-consent-all`,
175
+ `data-marketing-consent-item="<key>"`, `data-marketing-consent-updated="<key>"`,
176
+ `data-marketing-consent-save`, `data-marketing-consent-skip` (screen only — preferences has none),
177
+ `data-marketing-consent-error` (screen; preferences uses a bare `role="alert"`, no fixed id),
178
+ `data-marketing-consent-preferences-save`. `-all`/`-item`/`-updated` are the SAME attributes in
179
+ both the screen and `MarketingConsentPreferences` — the two are never mounted on one page at once.
180
+ Do not rename them.
181
+
182
+ ## The `@source` line — a consumer rule
183
+
184
+ Same trap as `@owlmeans/web-oauth`/`@owlmeans/web-consent`: Tailwind's scanner reads the CSS root
185
+ plus `@source` directives and excludes `node_modules`, so a class used only inside this package
186
+ never reaches the app's stylesheet and the screen/card render unstyled with nothing in the
187
+ consumer's own sources to blame. Every consuming app adds:
188
+
189
+ ```css
190
+ @source "../../../node_modules/@owlmeans/web-marketing-consent/src";
191
+ ```
192
+
193
+ Point at the shipped `src`, **never** `build` — under a linked workspace the `node_modules` entry
194
+ is a symlink into this monorepo, where every package's `build` directory is ignored by `.gitignore`,
195
+ so a `build` source scans zero files and reports nothing; the failure is silent. Primitives
196
+ (`button`, `card`, `label`) are private under `src/@/` and imported only by relative specifiers —
197
+ checkbox rows are a bare native `<input type="checkbox">`, the same choice `@owlmeans/client-auth`'s
198
+ own `LoginTerms` makes for a sign-in screen (no checkbox primitive, and its Radix peer, is vendored
199
+ just to render one). `tests/package-boundary.spec.ts` fails on any `from '@/…'` in `src/`.
200
+
201
+ ## Testing
202
+
203
+ Category D (`/testing-ui`) — `bun test ./tests` drives a real chromium against a real app, with
204
+ only the server absent, the same harness shape as `@owlmeans/web-oauth`'s:
205
+
206
+ - **Harness** (`tests/context.ts` boots Vite; `tests/harness/mount.tsx` is the app) — the real
207
+ `@owlmeans/web-client` context, `appendMarketingConsent(context, { protocols })` (so the step and
208
+ both landing hooks are really registered, even though no spec drives them directly), the
209
+ marketing-consent API bound with `bindAll` under a backend parent on the harness's own origin,
210
+ the screen's own entrypoint, a bare `home` screen, and a `/prefs` screen that renders
211
+ `MarketingConsentPreferences` for the preferences specs. `tests/helpers.ts`'s `open(path, {
212
+ stubs, signedIn, lng })` stubs `status`/`save`/`terms` via `page.route` on
213
+ `/\/api\/.*marketing-consent\//`; `statusView(overrides?)` builds a `MarketingConsentStatusView`
214
+ from `STANDARD_MARKETING_CONSENTS` (every standard key is `opt-in`, so the un-overridden default
215
+ IS "nothing checked").
216
+ - A signed-in visit is `?bearer=<makeBearer(USER)>`, written to IndexedDB (`idb-keyval`) BEFORE
217
+ `createRoot(...).render(...)` — **wait for a rendered marker** (`#home`, `[data-marketing-consent-save]`)
218
+ before `page.evaluate`-ing anything: `domcontentloaded` fires as soon as the module script
219
+ STARTS running, not once its own top-level `await`s (writing that bearer record) have settled: a
220
+ bare `page.evaluate` right after `goto` races the write and reads "signed out".
221
+ - **What is covered:** `screen.spec.ts` (nothing checked by default, select-all/indeterminate, the
222
+ `updated` badge, save posting every current draft value then continuing the flow to `/`, a failed
223
+ save showing error+skip with skip still moving the flow on, a framed mount rendering normally);
224
+ `preferences.spec.ts` (same body, no navigation on save, `onSaved` firing, no skip link exists);
225
+ `bridge.spec.ts` (non-browser — a fake `localStorage`/`document`/`window`, the same fixture
226
+ `@owlmeans/consent`'s own suite uses; read/write/the re-entrancy guard); `i18n.spec.ts`
227
+ (non-browser — 8-locale parity plus a literal-key scan of `t('screen.…'/'preferences.…')` calls
228
+ across the three component files); `service.spec.ts` (browser — drives the REAL registered
229
+ `MarketingConsentClientService` via `window.__mc`, since faking `assertCtx`/`context.entrypoint`
230
+ by hand is not worth it: signed-out never calls the API, a healthy server round-trips, a failing
231
+ server fails open on every method); `package-boundary.spec.ts` (no consumer `@/` aliases in
232
+ `src/`).
233
+
234
+ ## Related
235
+
236
+ - `@owlmeans/marketing-consent` — the catalogue, `consentStatus`'s decision table, the protocol
237
+ tree (read its skill first; this package repeats nothing it already documents)
238
+ - `@owlmeans/server-marketing-consent` — the service and handlers this package's `status`/`save`/
239
+ `terms` calls reach
240
+ - `@owlmeans/client-auth` (`./login`) — `LoginStep`/`LoginLandingHook`, `landAfterLogin`/
241
+ `continueLogin`/`useContinueLogin`, `resolveTerms`/`termsAccepted` — the seam this package plugs
242
+ into; read its skill for the bound-and-swallow contract every step/hook shares
243
+ - `@owlmeans/consent` — `consentStore`, `ConsentRecord`, the categories this bridge patches
244
+ - `@owlmeans/web-oauth` — the structural precedent this package copies: headless-hook/thin-component
245
+ split, vendored `src/@/` primitives, the `@source` line, the Vite+Playwright test harness shape
@@ -0,0 +1,11 @@
1
+ import * as React from 'react';
2
+ import { type VariantProps } from 'class-variance-authority';
3
+ declare const buttonVariants: (props?: ({
4
+ variant?: "default" | "destructive" | "ghost" | "link" | "outline" | "secondary" | null | undefined;
5
+ size?: "default" | "icon" | "lg" | "sm" | null | undefined;
6
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
+ declare function Button({ className, variant, size, asChild, ...props }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
8
+ asChild?: boolean;
9
+ }): React.JSX.Element;
10
+ export { Button, buttonVariants };
11
+ //# sourceMappingURL=button.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../../../../src/@/components/ui/button.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAIjE,QAAA,MAAM,cAAc;;;8EA6BnB,CAAA;AAED,iBAAS,MAAM,CAAC,EACd,SAAS,EACT,OAAO,EACP,IAAI,EACJ,OAAe,EACf,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,QAAQ,CAAC,GAC/B,YAAY,CAAC,OAAO,cAAc,CAAC,GAAG;IACpC,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,qBAUF;AAED,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAA"}
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Slot } from '@radix-ui/react-slot';
3
+ import { cva } from 'class-variance-authority';
4
+ import { cn } from '../../lib/utils.js';
5
+ const buttonVariants = cva("inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer", {
6
+ variants: {
7
+ variant: {
8
+ default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
9
+ destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
10
+ outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
11
+ secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
12
+ ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
13
+ link: "text-primary underline-offset-4 hover:underline",
14
+ },
15
+ size: {
16
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
17
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
18
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
19
+ icon: "size-9",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ size: "default",
25
+ },
26
+ });
27
+ function Button({ className, variant, size, asChild = false, ...props }) {
28
+ const Comp = asChild ? Slot : "button";
29
+ return (_jsx(Comp, { "data-slot": "button", className: cn(buttonVariants({ variant, size, className })), ...props }));
30
+ }
31
+ export { Button, buttonVariants };
32
+ //# sourceMappingURL=button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"button.js","sourceRoot":"","sources":["../../../../src/@/components/ui/button.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAC3C,OAAO,EAAE,GAAG,EAAqB,MAAM,0BAA0B,CAAA;AAEjE,OAAO,EAAE,EAAE,EAAE,MAAM,oBAAoB,CAAA;AAEvC,MAAM,cAAc,GAAG,GAAG,CACxB,4cAA4c,EAC5c;IACE,QAAQ,EAAE;QACR,OAAO,EAAE;YACP,OAAO,EACL,kEAAkE;YACpE,WAAW,EACT,6JAA6J;YAC/J,OAAO,EACL,uIAAuI;YACzI,SAAS,EACP,wEAAwE;YAC1E,KAAK,EACH,sEAAsE;YACxE,IAAI,EAAE,iDAAiD;SACxD;QACD,IAAI,EAAE;YACJ,OAAO,EAAE,+BAA+B;YACxC,EAAE,EAAE,+CAA+C;YACnD,EAAE,EAAE,sCAAsC;YAC1C,IAAI,EAAE,QAAQ;SACf;KACF;IACD,eAAe,EAAE;QACf,OAAO,EAAE,SAAS;QAClB,IAAI,EAAE,SAAS;KAChB;CACF,CACF,CAAA;AAED,SAAS,MAAM,CAAC,EACd,SAAS,EACT,OAAO,EACP,IAAI,EACJ,OAAO,GAAG,KAAK,EACf,GAAG,KAAK,EAIP;IACD,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAA;IAEtC,OAAO,CACL,KAAC,IAAI,iBACO,QAAQ,EAClB,SAAS,EAAE,EAAE,CAAC,cAAc,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,KACvD,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAA"}
@@ -0,0 +1,10 @@
1
+ import * as React from 'react';
2
+ declare function Card({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
3
+ declare function CardHeader({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
4
+ declare function CardTitle({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
5
+ declare function CardDescription({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
6
+ declare function CardAction({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
7
+ declare function CardContent({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
8
+ declare function CardFooter({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
9
+ export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent, };
10
+ //# sourceMappingURL=card.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"card.d.ts","sourceRoot":"","sources":["../../../../src/@/components/ui/card.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,iBAAS,IAAI,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAWjE;AAED,iBAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAWvE;AAED,iBAAS,SAAS,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAQtE;AAED,iBAAS,eAAe,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAQ5E;AAED,iBAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAWvE;AAED,iBAAS,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAQxE;AAED,iBAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAQvE;AAED,OAAO,EACL,IAAI,EACJ,UAAU,EACV,UAAU,EACV,SAAS,EACT,UAAU,EACV,eAAe,EACf,WAAW,GACZ,CAAA"}
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cn } from '../../lib/utils.js';
3
+ function Card({ className, ...props }) {
4
+ return (_jsx("div", { "data-slot": "card", className: cn("bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", className), ...props }));
5
+ }
6
+ function CardHeader({ className, ...props }) {
7
+ return (_jsx("div", { "data-slot": "card-header", className: cn("@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", className), ...props }));
8
+ }
9
+ function CardTitle({ className, ...props }) {
10
+ return (_jsx("div", { "data-slot": "card-title", className: cn("leading-none font-semibold", className), ...props }));
11
+ }
12
+ function CardDescription({ className, ...props }) {
13
+ return (_jsx("div", { "data-slot": "card-description", className: cn("text-muted-foreground text-sm", className), ...props }));
14
+ }
15
+ function CardAction({ className, ...props }) {
16
+ return (_jsx("div", { "data-slot": "card-action", className: cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className), ...props }));
17
+ }
18
+ function CardContent({ className, ...props }) {
19
+ return (_jsx("div", { "data-slot": "card-content", className: cn("px-6", className), ...props }));
20
+ }
21
+ function CardFooter({ className, ...props }) {
22
+ return (_jsx("div", { "data-slot": "card-footer", className: cn("flex items-center px-6 [.border-t]:pt-6", className), ...props }));
23
+ }
24
+ export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent, };
25
+ //# sourceMappingURL=card.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"card.js","sourceRoot":"","sources":["../../../../src/@/components/ui/card.tsx"],"names":[],"mappings":";AAGA,OAAO,EAAE,EAAE,EAAE,MAAM,oBAAoB,CAAA;AAEvC,SAAS,IAAI,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IAChE,OAAO,CACL,2BACY,MAAM,EAChB,SAAS,EAAE,EAAE,CACX,mFAAmF,EACnF,SAAS,CACV,KACG,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,SAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IACtE,OAAO,CACL,2BACY,aAAa,EACvB,SAAS,EAAE,EAAE,CACX,4JAA4J,EAC5J,SAAS,CACV,KACG,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,SAAS,SAAS,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IACrE,OAAO,CACL,2BACY,YAAY,EACtB,SAAS,EAAE,EAAE,CAAC,4BAA4B,EAAE,SAAS,CAAC,KAClD,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,SAAS,eAAe,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IAC3E,OAAO,CACL,2BACY,kBAAkB,EAC5B,SAAS,EAAE,EAAE,CAAC,+BAA+B,EAAE,SAAS,CAAC,KACrD,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,SAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IACtE,OAAO,CACL,2BACY,aAAa,EACvB,SAAS,EAAE,EAAE,CACX,gEAAgE,EAChE,SAAS,CACV,KACG,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,SAAS,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IACvE,OAAO,CACL,2BACY,cAAc,EACxB,SAAS,EAAE,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,KAC5B,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,SAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IACtE,OAAO,CACL,2BACY,aAAa,EACvB,SAAS,EAAE,EAAE,CAAC,yCAAyC,EAAE,SAAS,CAAC,KAC/D,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,OAAO,EACL,IAAI,EACJ,UAAU,EACV,UAAU,EACV,SAAS,EACT,UAAU,EACV,eAAe,EACf,WAAW,GACZ,CAAA"}
@@ -0,0 +1,5 @@
1
+ import * as React from 'react';
2
+ import * as LabelPrimitive from '@radix-ui/react-label';
3
+ declare function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>): React.JSX.Element;
4
+ export { Label };
5
+ //# sourceMappingURL=label.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"label.d.ts","sourceRoot":"","sources":["../../../../src/@/components/ui/label.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,cAAc,MAAM,uBAAuB,CAAA;AAIvD,iBAAS,KAAK,CAAC,EACb,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,cAAc,CAAC,IAAI,CAAC,qBAWlD;AAED,OAAO,EAAE,KAAK,EAAE,CAAA"}
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as LabelPrimitive from '@radix-ui/react-label';
3
+ import { cn } from '../../lib/utils.js';
4
+ function Label({ className, ...props }) {
5
+ return (_jsx(LabelPrimitive.Root, { "data-slot": "label", className: cn("flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", className), ...props }));
6
+ }
7
+ export { Label };
8
+ //# sourceMappingURL=label.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"label.js","sourceRoot":"","sources":["../../../../src/@/components/ui/label.tsx"],"names":[],"mappings":";AAEA,OAAO,KAAK,cAAc,MAAM,uBAAuB,CAAA;AAEvD,OAAO,EAAE,EAAE,EAAE,MAAM,oBAAoB,CAAA;AAEvC,SAAS,KAAK,CAAC,EACb,SAAS,EACT,GAAG,KAAK,EACyC;IACjD,OAAO,CACL,KAAC,cAAc,CAAC,IAAI,iBACR,OAAO,EACjB,SAAS,EAAE,EAAE,CACX,qNAAqN,EACrN,SAAS,CACV,KACG,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,OAAO,EAAE,KAAK,EAAE,CAAA"}
@@ -0,0 +1,3 @@
1
+ import { type ClassValue } from 'clsx';
2
+ export declare function cn(...inputs: ClassValue[]): string;
3
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/@/lib/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,MAAM,CAAA;AAG5C,wBAAgB,EAAE,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,UAEzC"}
@@ -0,0 +1,6 @@
1
+ import { clsx } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+ export function cn(...inputs) {
4
+ return twMerge(clsx(inputs));
5
+ }
6
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../../src/@/lib/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,MAAM,CAAA;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAExC,MAAM,UAAU,EAAE,CAAC,GAAG,MAAoB;IACxC,OAAO,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAA;AAC9B,CAAC"}
@@ -0,0 +1,32 @@
1
+ import type { ClientConfig, ClientContext } from '@owlmeans/client-context';
2
+ import type { MarketingConsentBridge, MarketingConsentConfig, MarketingConsentEntrypoints } from '@owlmeans/marketing-consent';
3
+ export interface MarketingConsentAppendOptions {
4
+ /** From `makeMarketingConsentProtocols(...)` — the app builds this once and shares it between
5
+ * this call and its own server-side `serveMarketingConsentEntrypoints`/`bindAll` wiring. */
6
+ protocols: MarketingConsentEntrypoints;
7
+ /**
8
+ * Accepted for API symmetry with the server side (`appendMarketingConsentService({ config })`),
9
+ * but not read here: `resolveMarketingConsents` only ever runs where the catalogue is actually
10
+ * enumerated — the server. This package's own UI reads the ALREADY-RESOLVED definitions back off
11
+ * `MarketingConsentStatusItem.definition` and never re-resolves the catalogue client-side.
12
+ */
13
+ config?: MarketingConsentConfig;
14
+ /** Register the post-sign-in step. Default `true`. */
15
+ step?: boolean;
16
+ /** Record a fresh terms acceptance on landing. Default `true`. */
17
+ terms?: boolean;
18
+ /** Default `[cookieConsentBridge()]`. */
19
+ bridges?: MarketingConsentBridge[];
20
+ /** Entrypoint alias of a host's own "Privacy choices" settings screen, for
21
+ * `MarketingConsentClientService.preferences()`. */
22
+ preferences?: string;
23
+ locale?: string;
24
+ }
25
+ /**
26
+ * Wire the marketing-consent client into a web app: the client service, the post-sign-in step
27
+ * (pending while anything is unanswered) and the two landing hooks (terms acceptance, then
28
+ * cookie/account reconciliation) — the whole of what an application calls beyond registering
29
+ * `marketingConsentEntrypoints(protocols)` in its own entrypoint list.
30
+ */
31
+ export declare const appendMarketingConsent: <C extends ClientConfig, T extends ClientContext<C>>(context: T, opts: MarketingConsentAppendOptions) => T;
32
+ //# sourceMappingURL=append.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"append.d.ts","sourceRoot":"","sources":["../src/append.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAE3E,OAAO,KAAK,EACV,sBAAsB,EAAE,sBAAsB,EAAE,2BAA2B,EAC5E,MAAM,6BAA6B,CAAA;AAQpC,MAAM,WAAW,6BAA6B;IAC5C;gGAC4F;IAC5F,SAAS,EAAE,2BAA2B,CAAA;IACtC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,sBAAsB,CAAA;IAC/B,sDAAsD;IACtD,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,kEAAkE;IAClE,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,yCAAyC;IACzC,OAAO,CAAC,EAAE,sBAAsB,EAAE,CAAA;IAClC;wDACoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GAAI,CAAC,SAAS,YAAY,EAAE,CAAC,SAAS,aAAa,CAAC,CAAC,CAAC,WAC9E,CAAC,QAAQ,6BAA6B,KAC9C,CAiBF,CAAA"}
@@ -0,0 +1,28 @@
1
+ import { ensureLoginService } from '@owlmeans/client-auth/login';
2
+ import { cookieConsentBridge } from './bridge.js';
3
+ import { MARKETING_CONSENT_CLIENT_SERVICE } from './consts.js';
4
+ import { landingSync, termsRecorder } from './landing.js';
5
+ import { appendMarketingConsentClient } from './service.js';
6
+ import { marketingConsentStep } from './step.js';
7
+ /**
8
+ * Wire the marketing-consent client into a web app: the client service, the post-sign-in step
9
+ * (pending while anything is unanswered) and the two landing hooks (terms acceptance, then
10
+ * cookie/account reconciliation) — the whole of what an application calls beyond registering
11
+ * `marketingConsentEntrypoints(protocols)` in its own entrypoint list.
12
+ */
13
+ export const appendMarketingConsent = (context, opts) => {
14
+ const bridges = opts.bridges ?? [cookieConsentBridge()];
15
+ appendMarketingConsentClient(context, opts.protocols, { bridges, preferences: opts.preferences });
16
+ const client = context.service(MARKETING_CONSENT_CLIENT_SERVICE);
17
+ const login = ensureLoginService(context);
18
+ if (opts.step !== false) {
19
+ login.registerStep(marketingConsentStep(client, opts.protocols.screen.alias));
20
+ }
21
+ if (opts.terms !== false) {
22
+ const terms = context.cfg.security?.auth?.login?.terms;
23
+ login.onLanded(termsRecorder(terms, client, opts.locale));
24
+ }
25
+ login.onLanded(landingSync(client));
26
+ return context;
27
+ };
28
+ //# sourceMappingURL=append.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"append.js","sourceRoot":"","sources":["../src/append.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAA;AAIhE,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AACjD,OAAO,EAAE,gCAAgC,EAAE,MAAM,aAAa,CAAA;AAC9D,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,EAAE,4BAA4B,EAAE,MAAM,cAAc,CAAA;AAE3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAyBhD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CACpC,OAAU,EAAE,IAAmC,EAC5C,EAAE;IACL,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,CAAC,mBAAmB,EAAE,CAAC,CAAA;IAEvD,4BAA4B,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,EAAE,EAAE,OAAO,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAA;IACjG,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAgC,gCAAgC,CAAC,CAAA;IAE/F,MAAM,KAAK,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAA;IACzC,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;QACxB,KAAK,CAAC,YAAY,CAAC,oBAAoB,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;IAC/E,CAAC;IACD,IAAI,IAAI,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAkC,OAAO,CAAC,GAAoB,CAAC,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,CAAA;QACtG,KAAK,CAAC,QAAQ,CAAC,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAA;IAC3D,CAAC;IACD,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAA;IAEnC,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA"}
@@ -0,0 +1,25 @@
1
+ import type { ConsentOptions } from '@owlmeans/consent';
2
+ import type { MarketingConsentBridge } from '@owlmeans/marketing-consent';
3
+ export interface MakeCookieConsentBridgeOptions {
4
+ /**
5
+ * The SAME options the app passes to `consentStore.init(...)` elsewhere (e.g. via
6
+ * `@owlmeans/web-consent`'s dialog mount) — read here only to find which category key is the
7
+ * required "essential" one, so `write` re-asserts the right key even when a host renamed it.
8
+ * This bridge never calls `consentStore.init` itself; a marketing-consent screen has no opinion
9
+ * about whether a cookie dialog exists at all.
10
+ */
11
+ storageOptions?: ConsentOptions;
12
+ }
13
+ /**
14
+ * The seam between a person's saved marketing-consent DECISIONS (server-held, per account) and
15
+ * this browser's cookie-consent RECORD (`@owlmeans/consent`'s `consentStore`, per device) — for
16
+ * every definition whose `cookieCategory` names a `@owlmeans/consent` category
17
+ * (`trackers.analytics` -> `CONSENT_ANALYTICS`, `trackers.advertising` -> `CONSENT_MARKETING`).
18
+ *
19
+ * `consentStore.save` REPLACES the stored record wholesale — `store.ts`'s `save` calls
20
+ * `writeConsent(record, options)` with exactly what it is given, no merge of its own — so `write`
21
+ * always spreads the CURRENT record first and always re-asserts the essential category, before
22
+ * patching in only the categories THIS bridge owns.
23
+ */
24
+ export declare const cookieConsentBridge: (opts?: MakeCookieConsentBridgeOptions) => MarketingConsentBridge;
25
+ //# sourceMappingURL=bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAiB,MAAM,mBAAmB,CAAA;AACtE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAA;AAEzE,MAAM,WAAW,8BAA8B;IAC7C;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,cAAc,CAAA;CAChC;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,UAAW,8BAA8B,KAAG,sBA8D3E,CAAA"}
@@ -0,0 +1,69 @@
1
+ import { CONSENT_ESSENTIAL, DEFAULT_CONSENT_CATEGORIES, consentStore } from '@owlmeans/consent';
2
+ /**
3
+ * The seam between a person's saved marketing-consent DECISIONS (server-held, per account) and
4
+ * this browser's cookie-consent RECORD (`@owlmeans/consent`'s `consentStore`, per device) — for
5
+ * every definition whose `cookieCategory` names a `@owlmeans/consent` category
6
+ * (`trackers.analytics` -> `CONSENT_ANALYTICS`, `trackers.advertising` -> `CONSENT_MARKETING`).
7
+ *
8
+ * `consentStore.save` REPLACES the stored record wholesale — `store.ts`'s `save` calls
9
+ * `writeConsent(record, options)` with exactly what it is given, no merge of its own — so `write`
10
+ * always spreads the CURRENT record first and always re-asserts the essential category, before
11
+ * patching in only the categories THIS bridge owns.
12
+ */
13
+ export const cookieConsentBridge = (opts) => {
14
+ const categories = opts?.storageOptions?.categories ?? DEFAULT_CONSENT_CATEGORIES;
15
+ const essentialKey = categories.find(category => category.required === true)?.key ?? CONSENT_ESSENTIAL;
16
+ // Re-entrancy guard: `write` calling `consentStore.save` publishes to every subscriber of the
17
+ // store, including the one `subscribe` below registers — without this, saving a marketing
18
+ // decision through this bridge would immediately hand the SAME change back to a caller that
19
+ // reacts to it by writing again (a naive two-way binding), looping the two writes forever.
20
+ let suppress = false;
21
+ return {
22
+ alias: 'cookie-consent',
23
+ read: defs => {
24
+ const record = consentStore.get().record;
25
+ if (record == null) {
26
+ return null;
27
+ }
28
+ const out = {};
29
+ for (const definition of defs) {
30
+ if (definition.cookieCategory != null) {
31
+ out[definition.key] = record[definition.cookieCategory] === true;
32
+ }
33
+ }
34
+ return out;
35
+ },
36
+ write: (decisions, defs) => {
37
+ const current = consentStore.get().record ?? {};
38
+ const patch = {};
39
+ for (const definition of defs) {
40
+ if (definition.cookieCategory != null && decisions[definition.key] != null) {
41
+ patch[definition.cookieCategory] = decisions[definition.key];
42
+ }
43
+ }
44
+ if (Object.keys(patch).length === 0) {
45
+ return;
46
+ }
47
+ suppress = true;
48
+ try {
49
+ consentStore.save({ ...current, [essentialKey]: true, ...patch });
50
+ }
51
+ finally {
52
+ suppress = false;
53
+ }
54
+ },
55
+ subscribe: (listener, defs) => consentStore.subscribe(state => {
56
+ if (suppress || state.record == null) {
57
+ return;
58
+ }
59
+ const out = {};
60
+ for (const definition of defs) {
61
+ if (definition.cookieCategory != null) {
62
+ out[definition.key] = state.record[definition.cookieCategory] === true;
63
+ }
64
+ }
65
+ listener(out);
66
+ }),
67
+ };
68
+ };
69
+ //# sourceMappingURL=bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,0BAA0B,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAe/F;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,IAAqC,EAA0B,EAAE;IACnG,MAAM,UAAU,GAAG,IAAI,EAAE,cAAc,EAAE,UAAU,IAAI,0BAA0B,CAAA;IACjF,MAAM,YAAY,GAAG,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,QAAQ,KAAK,IAAI,CAAC,EAAE,GAAG,IAAI,iBAAiB,CAAA;IAEtG,8FAA8F;IAC9F,0FAA0F;IAC1F,4FAA4F;IAC5F,2FAA2F;IAC3F,IAAI,QAAQ,GAAG,KAAK,CAAA;IAEpB,OAAO;QACL,KAAK,EAAE,gBAAgB;QAEvB,IAAI,EAAE,IAAI,CAAC,EAAE;YACX,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,EAAE,CAAC,MAAM,CAAA;YACxC,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;gBACnB,OAAO,IAAI,CAAA;YACb,CAAC;YAED,MAAM,GAAG,GAA4B,EAAE,CAAA;YACvC,KAAK,MAAM,UAAU,IAAI,IAAI,EAAE,CAAC;gBAC9B,IAAI,UAAU,CAAC,cAAc,IAAI,IAAI,EAAE,CAAC;oBACtC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,cAAc,CAAC,KAAK,IAAI,CAAA;gBAClE,CAAC;YACH,CAAC;YAED,OAAO,GAAG,CAAA;QACZ,CAAC;QAED,KAAK,EAAE,CAAC,SAAS,EAAE,IAAI,EAAE,EAAE;YACzB,MAAM,OAAO,GAAkB,YAAY,CAAC,GAAG,EAAE,CAAC,MAAM,IAAI,EAAE,CAAA;YAC9D,MAAM,KAAK,GAA2B,EAAE,CAAA;YACxC,KAAK,MAAM,UAAU,IAAI,IAAI,EAAE,CAAC;gBAC9B,IAAI,UAAU,CAAC,cAAc,IAAI,IAAI,IAAI,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;oBAC3E,KAAK,CAAC,UAAU,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC,CAAA;gBAC9D,CAAC;YACH,CAAC;YACD,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACpC,OAAM;YACR,CAAC;YAED,QAAQ,GAAG,IAAI,CAAA;YACf,IAAI,CAAC;gBACH,YAAY,CAAC,IAAI,CAAC,EAAE,GAAG,OAAO,EAAE,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,GAAG,KAAK,EAAE,CAAC,CAAA;YACnE,CAAC;oBAAS,CAAC;gBACT,QAAQ,GAAG,KAAK,CAAA;YAClB,CAAC;QACH,CAAC;QAED,SAAS,EAAE,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE;YAC5D,IAAI,QAAQ,IAAI,KAAK,CAAC,MAAM,IAAI,IAAI,EAAE,CAAC;gBACrC,OAAM;YACR,CAAC;YACD,MAAM,GAAG,GAA4B,EAAE,CAAA;YACvC,KAAK,MAAM,UAAU,IAAI,IAAI,EAAE,CAAC;gBAC9B,IAAI,UAAU,CAAC,cAAc,IAAI,IAAI,EAAE,CAAC;oBACtC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,cAAc,CAAC,KAAK,IAAI,CAAA;gBACxE,CAAC;YACH,CAAC;YACD,QAAQ,CAAC,GAAG,CAAC,CAAA;QACf,CAAC,CAAC;KACH,CAAA;AACH,CAAC,CAAA"}