@revenexx/cover 0.1.1 → 0.1.2

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.
@@ -1,8 +1,17 @@
1
1
  <script setup lang="ts">
2
2
  import { accountNavigation } from "../../config/navigation";
3
+ import type { AccountNavGroup } from "../../config/navigation";
3
4
 
4
5
  const { icon } = useIcons();
5
6
 
7
+ // Consumers can replace the full B2B navigation tree with their own
8
+ // (e.g. a lean storefront theme exposing only orders + profile) via
9
+ // app.config — `accountNavigation: AccountNavGroup[]`.
10
+ const appConfig = useAppConfig();
11
+ const navigation = computed<AccountNavGroup[]>(() =>
12
+ (appConfig.accountNavigation as AccountNavGroup[] | undefined) ?? accountNavigation,
13
+ );
14
+
6
15
  /**
7
16
  * The full B2B account navigation tree (structure from the relation
8
17
  * prototype). Implemented entries route to real pages, the rest leads to the
@@ -41,7 +50,7 @@ function isActive(to: string): boolean {
41
50
  >
42
51
  <ul class="divide-y divide-default">
43
52
  <li
44
- v-for="group in accountNavigation"
53
+ v-for="group in navigation"
45
54
  :key="group.labelKey"
46
55
  class="py-4"
47
56
  >
package/nuxt.config.ts CHANGED
@@ -95,6 +95,13 @@ export default defineNuxtConfig({
95
95
  publicAssets: [
96
96
  { dir: resolve(currentDir, "public") },
97
97
  ],
98
+ // Per-request async context: lets server utils reach the current
99
+ // H3 event without threading it through every signature — the SDK
100
+ // client uses it to pick up the platform's brokered tenant context
101
+ // (x-revenexx-tenant / x-revenexx-context headers, ADR-0057 §8).
102
+ experimental: {
103
+ asyncContext: true,
104
+ },
98
105
  },
99
106
 
100
107
  i18n: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revenexx/cover",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Cover \u2014 revenexx design system for Nuxt. Distributed as a Nuxt layer: generic UI components, theming tokens and stores shared by the demo shop, custom storefronts and the Blokkli theme.",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
@@ -25,7 +25,9 @@ interface ApiListPage<T> {
25
25
 
26
26
  const MARKET_CACHE_TTL_MS = 5 * 60 * 1000;
27
27
 
28
- let cache: { markets: ShopMarket[]; loadedAt: number } | null = null;
28
+ // Partitioned by tenant a multi-tenant theme must never serve one
29
+ // tenant's markets to another (see revenexxTenantKey()).
30
+ const cache = new Map<string, { markets: ShopMarket[]; loadedAt: number }>();
29
31
 
30
32
  /**
31
33
  * Live markets via the public revenexx API (markets app):
@@ -34,8 +36,10 @@ let cache: { markets: ShopMarket[]; loadedAt: number } | null = null;
34
36
  */
35
37
  export class ApiMarketService implements IMarketService {
36
38
  async listMarkets(): Promise<ShopMarket[]> {
37
- if (cache && Date.now() - cache.loadedAt < MARKET_CACHE_TTL_MS) {
38
- return cache.markets;
39
+ const tenantKey = revenexxTenantKey();
40
+ const cached = cache.get(tenantKey);
41
+ if (cached && Date.now() - cached.loadedAt < MARKET_CACHE_TTL_MS) {
42
+ return cached.markets;
39
43
  }
40
44
 
41
45
  const sdk = useRevenexxSdk();
@@ -63,7 +67,7 @@ export class ApiMarketService implements IMarketService {
63
67
  };
64
68
  }));
65
69
 
66
- cache = { markets, loadedAt: Date.now() };
70
+ cache.set(tenantKey, { markets, loadedAt: Date.now() });
67
71
  return markets;
68
72
  }
69
73
  }
@@ -175,11 +175,15 @@ interface LiveCategoryPage {
175
175
  const CATEGORY_CACHE_TTL_MS = 5 * 60 * 1000;
176
176
  const CATEGORY_PAGE_LIMIT = 200;
177
177
 
178
- let categoryCache: { rows: LiveCategoryRow[]; loadedAt: number } | null = null;
178
+ // Partitioned by tenant a multi-tenant theme must never serve one
179
+ // tenant's category tree to another (see revenexxTenantKey()).
180
+ const categoryCache = new Map<string, { rows: LiveCategoryRow[]; loadedAt: number }>();
179
181
 
180
182
  async function loadLiveCategoryRows(): Promise<LiveCategoryRow[]> {
181
- if (categoryCache && Date.now() - categoryCache.loadedAt < CATEGORY_CACHE_TTL_MS) {
182
- return categoryCache.rows;
183
+ const tenantKey = revenexxTenantKey();
184
+ const cached = categoryCache.get(tenantKey);
185
+ if (cached && Date.now() - cached.loadedAt < CATEGORY_CACHE_TTL_MS) {
186
+ return cached.rows;
183
187
  }
184
188
 
185
189
  const sdk = useRevenexxSdk();
@@ -199,7 +203,7 @@ async function loadLiveCategoryRows(): Promise<LiveCategoryRow[]> {
199
203
  offset += page.page.returned;
200
204
  }
201
205
 
202
- categoryCache = { rows, loadedAt: Date.now() };
206
+ categoryCache.set(tenantKey, { rows, loadedAt: Date.now() });
203
207
  return rows;
204
208
  }
205
209
 
@@ -91,17 +91,58 @@ export function apiErrorContext(err: unknown): Record<string, unknown> {
91
91
 
92
92
  let sdk: RevenexxSdk | null = null;
93
93
 
94
+ /**
95
+ * The platform's per-invocation tenant context (ADR-0057 §8): on a deployed
96
+ * Site the entrypoint resolves the tenant from the request Host and injects
97
+ * x-revenexx-tenant the tenant slug
98
+ * x-revenexx-context the brokered per-tenant JWT for gateway calls
99
+ * When present, they win over the env credentials — the same theme build
100
+ * serves any tenant. Resolution uses Nitro's async context (no event
101
+ * threading); outside a request, or without the headers, the env-configured
102
+ * tenant API key applies (local dev, the demo shop, previews).
103
+ */
104
+ function brokeredContext(): { tenant: string; jwt: string } | null {
105
+ try {
106
+ const event = useEvent();
107
+ const tenant = getRequestHeader(event, "x-revenexx-tenant");
108
+ if (!tenant) {
109
+ return null;
110
+ }
111
+ return { tenant, jwt: getRequestHeader(event, "x-revenexx-context") ?? "" };
112
+ }
113
+ catch {
114
+ // No async context (e.g. startup code) — fall back to env credentials.
115
+ return null;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * The tenant slug the current request's SDK calls are scoped to — the cache
121
+ * key for any server-side cache of gateway data (markets, categories, …).
122
+ * Module-level caches MUST be partitioned by this, or a multi-tenant theme
123
+ * leaks one tenant's data into another's pages.
124
+ */
125
+ export function revenexxTenantKey(): string {
126
+ const brokered = brokeredContext();
127
+ if (brokered) {
128
+ return brokered.tenant;
129
+ }
130
+ const config = useRuntimeConfig();
131
+ return String(config.revenexxTenant || "");
132
+ }
133
+
94
134
  export function useRevenexxSdk(): RevenexxSdk {
95
- if (sdk) {
135
+ const brokered = brokeredContext();
136
+ if (!brokered && sdk) {
96
137
  return sdk;
97
138
  }
98
139
 
99
140
  const config = useRuntimeConfig();
100
141
  const endpoint = String(config.revenexxApiUrl || "https://api.revenexx.com").replace(/\/+$/, "");
101
- const tenant = String(config.revenexxTenant || "");
142
+ const tenant = brokered?.tenant ?? String(config.revenexxTenant || "");
102
143
  const apiKey = String(config.revenexxApiKey || "");
103
144
 
104
- if (!tenant || !apiKey) {
145
+ if (!tenant || (!brokered?.jwt && !apiKey)) {
105
146
  throw new RevenexxException(
106
147
  "revenexx API credentials are not configured (NUXT_REVENEXX_TENANT / NUXT_REVENEXX_API_KEY)",
107
148
  503,
@@ -112,8 +153,13 @@ export function useRevenexxSdk(): RevenexxSdk {
112
153
 
113
154
  const client = new Client()
114
155
  .setEndpoint(endpoint)
115
- .setTenant(tenant)
116
- .setApiKeyAuth(apiKey);
156
+ .setTenant(tenant);
157
+ if (brokered?.jwt) {
158
+ client.setBearerAuth(brokered.jwt);
159
+ }
160
+ else {
161
+ client.setApiKeyAuth(apiKey);
162
+ }
117
163
 
118
164
  async function call<T>(
119
165
  method: "GET" | "POST" | "PUT" | "DELETE",
@@ -144,7 +190,7 @@ export function useRevenexxSdk(): RevenexxSdk {
144
190
  ) as T;
145
191
  }
146
192
 
147
- sdk = {
193
+ const instance: RevenexxSdk = {
148
194
  client,
149
195
  carts: new Carts(client),
150
196
  customers: new Customers(client),
@@ -158,5 +204,10 @@ export function useRevenexxSdk(): RevenexxSdk {
158
204
  shipping: new Shipping(client),
159
205
  call,
160
206
  };
161
- return sdk;
207
+ // Brokered instances are request-scoped (the JWT rotates per invocation)
208
+ // — only the env-credential client is a process-wide singleton.
209
+ if (!brokered) {
210
+ sdk = instance;
211
+ }
212
+ return instance;
162
213
  }