@revenexx/cover 0.1.1 → 0.1.3
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
|
|
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.
|
|
3
|
+
"version": "0.1.3",
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
|
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
|
-
|
|
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,19 @@ export function useRevenexxSdk(): RevenexxSdk {
|
|
|
112
153
|
|
|
113
154
|
const client = new Client()
|
|
114
155
|
.setEndpoint(endpoint)
|
|
115
|
-
.setTenant(tenant)
|
|
116
|
-
|
|
156
|
+
.setTenant(tenant);
|
|
157
|
+
// Auth: the brokered context wins — it is a Zitadel JWT the gateway
|
|
158
|
+
// verifies against the same JWKS as any interactive caller, so one theme
|
|
159
|
+
// build serves any tenant without per-site credentials. The explicit
|
|
160
|
+
// "Bearer " prefix works around @revenexx/sdk <= 0.0.5 writing the raw
|
|
161
|
+
// value into the Authorization header (generator fix: sdk-generator #9;
|
|
162
|
+
// the fixed setter keeps an existing prefix, so this stays correct).
|
|
163
|
+
if (brokered?.jwt) {
|
|
164
|
+
client.setBearerAuth(`Bearer ${brokered.jwt}`);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
client.setApiKeyAuth(apiKey);
|
|
168
|
+
}
|
|
117
169
|
|
|
118
170
|
async function call<T>(
|
|
119
171
|
method: "GET" | "POST" | "PUT" | "DELETE",
|
|
@@ -144,7 +196,7 @@ export function useRevenexxSdk(): RevenexxSdk {
|
|
|
144
196
|
) as T;
|
|
145
197
|
}
|
|
146
198
|
|
|
147
|
-
|
|
199
|
+
const instance: RevenexxSdk = {
|
|
148
200
|
client,
|
|
149
201
|
carts: new Carts(client),
|
|
150
202
|
customers: new Customers(client),
|
|
@@ -158,5 +210,10 @@ export function useRevenexxSdk(): RevenexxSdk {
|
|
|
158
210
|
shipping: new Shipping(client),
|
|
159
211
|
call,
|
|
160
212
|
};
|
|
161
|
-
|
|
213
|
+
// Brokered instances are request-scoped (the JWT rotates per invocation)
|
|
214
|
+
// — only the env-credential client is a process-wide singleton.
|
|
215
|
+
if (!brokered) {
|
|
216
|
+
sdk = instance;
|
|
217
|
+
}
|
|
218
|
+
return instance;
|
|
162
219
|
}
|