@revenexx/cover 0.1.0 → 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.
Files changed (42) hide show
  1. package/app/app.config.ts +1 -1
  2. package/app/components/account/AccountSidebar.vue +10 -1
  3. package/app/components/account/address/AccountAddressCard.vue +69 -88
  4. package/app/components/auth/AuthRegisterPanel.vue +122 -194
  5. package/app/formkit.config.ts +21 -0
  6. package/app/interfaces/auth.ts +0 -2
  7. package/app/interfaces/validation.ts +1 -1
  8. package/app/validations/formValidationConfig.ts +0 -30
  9. package/nuxt.config.ts +7 -3
  10. package/package.json +2 -2
  11. package/server/api/account/orders.get.ts +6 -5
  12. package/server/api/account/profile.get.ts +3 -3
  13. package/server/api/account/profile.put.ts +6 -4
  14. package/server/api/auth/login.post.ts +2 -5
  15. package/server/api/auth/recovery.post.ts +5 -13
  16. package/server/api/auth/recovery.put.ts +5 -14
  17. package/server/api/auth/register.post.ts +21 -44
  18. package/server/api/orders/index.post.ts +28 -24
  19. package/server/api/payment/methods.post.ts +5 -4
  20. package/server/api/shipping/rates.post.ts +6 -4
  21. package/server/interfaces/auth.ts +1 -1
  22. package/server/services/ApiAccountService.ts +44 -0
  23. package/server/services/ApiAuthService.ts +15 -14
  24. package/server/services/ApiCartService.ts +45 -27
  25. package/server/services/ApiMarketService.ts +11 -9
  26. package/server/utils/accountService.ts +4 -5
  27. package/server/utils/authService.ts +0 -3
  28. package/server/utils/liveCatalog.ts +30 -17
  29. package/server/utils/liveInventories.ts +4 -4
  30. package/server/utils/liveOrders.ts +5 -3
  31. package/server/utils/livePrices.ts +10 -9
  32. package/server/utils/revenexxSdk.ts +213 -0
  33. package/app/validations/companyName.ts +0 -18
  34. package/app/validations/maxLength.ts +0 -12
  35. package/app/validations/optionalCompanyName.ts +0 -23
  36. package/app/validations/phoneNumber.ts +0 -19
  37. package/app/validations/termsRequired.ts +0 -4
  38. package/app/validations/zipCode.ts +0 -15
  39. package/server/services/SdkAccountService.ts +0 -56
  40. package/server/services/SdkAuthService.ts +0 -83
  41. package/server/utils/revenexxApi.ts +0 -136
  42. package/server/utils/shopSdk.ts +0 -88
@@ -0,0 +1,213 @@
1
+ import {
2
+ Carts,
3
+ Client,
4
+ Customers,
5
+ Inventories,
6
+ Markets,
7
+ Orders,
8
+ Payments,
9
+ Prices,
10
+ Products,
11
+ RevenexxException,
12
+ Search,
13
+ Shipping,
14
+ } from "@revenexx/sdk";
15
+
16
+ /**
17
+ * The generated revenexx web SDK, configured for the BFF. Every live
18
+ * "api"-keyed implementation goes through these service instances.
19
+ *
20
+ * Auth model: tenant API key (server-side only, never reaches the client).
21
+ * Configure via runtime config / env:
22
+ * NUXT_REVENEXX_API_URL (default https://api.revenexx.com)
23
+ * NUXT_REVENEXX_TENANT (tenant slug, e.g. "revenexx")
24
+ * NUXT_REVENEXX_API_KEY (rvxk_… gateway key)
25
+ */
26
+
27
+ // Re-exported so route handlers can narrow SDK failures without importing
28
+ // from the package themselves (Nitro auto-imports this util's exports).
29
+ export { RevenexxException };
30
+
31
+ export interface RevenexxSdkCallOptions {
32
+ readonly query?: Record<string, string | number | boolean | undefined>;
33
+ readonly body?: unknown;
34
+ }
35
+
36
+ export interface RevenexxSdk {
37
+ readonly client: Client;
38
+ readonly carts: Carts;
39
+ readonly customers: Customers;
40
+ readonly inventories: Inventories;
41
+ readonly markets: Markets;
42
+ readonly orders: Orders;
43
+ readonly payments: Payments;
44
+ readonly prices: Prices;
45
+ readonly products: Products;
46
+ readonly search: Search;
47
+ readonly shipping: Shipping;
48
+ /**
49
+ * Low-level escape hatch over the SDK transport (same auth, same error
50
+ * type) for the few operations whose gateway contracts are not complete
51
+ * yet — list query parameters and auth/me's session check. Each call
52
+ * site documents the missing contract piece; once the apps declare it
53
+ * and the SDK regenerates, the call moves to the typed method.
54
+ */
55
+ call<T>(method: "GET" | "POST" | "PUT" | "DELETE", path: string, options?: RevenexxSdkCallOptions): Promise<T>;
56
+ }
57
+
58
+ /**
59
+ * Expected user-facing failures (wrong credentials, duplicate email, …) are
60
+ * forwarded to the client as-is; everything else is an infrastructure error.
61
+ */
62
+ const API_USER_ERROR_STATUS = new Set([400, 401, 404, 409, 429]);
63
+
64
+ export function isApiUserError(err: unknown): err is RevenexxException {
65
+ return err instanceof RevenexxException && API_USER_ERROR_STATUS.has(err.code);
66
+ }
67
+
68
+ /**
69
+ * The customers app's auth passthrough answers with a generic error envelope
70
+ * (no machine type). Map the HTTP status onto the platform error types the
71
+ * client-side auth store already translates, so mock/api modes produce
72
+ * identical UX for the standard failures.
73
+ */
74
+ export function apiAuthErrorType(err: RevenexxException): string {
75
+ switch (err.code) {
76
+ case 401: return "user_invalid_credentials";
77
+ case 404: return "user_not_found";
78
+ case 409: return "user_email_already_exists";
79
+ case 429: return "general_rate_limit_exceeded";
80
+ default: return err.type || "api_error";
81
+ }
82
+ }
83
+
84
+ /** Diagnostic fields for structured logs — never includes the API key. */
85
+ export function apiErrorContext(err: unknown): Record<string, unknown> {
86
+ if (err instanceof RevenexxException) {
87
+ return { apiStatus: err.code, apiType: err.type, apiMessage: err.message };
88
+ }
89
+ return { errorMessage: err instanceof Error ? err.message : String(err) };
90
+ }
91
+
92
+ let sdk: RevenexxSdk | null = null;
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
+
134
+ export function useRevenexxSdk(): RevenexxSdk {
135
+ const brokered = brokeredContext();
136
+ if (!brokered && sdk) {
137
+ return sdk;
138
+ }
139
+
140
+ const config = useRuntimeConfig();
141
+ const endpoint = String(config.revenexxApiUrl || "https://api.revenexx.com").replace(/\/+$/, "");
142
+ const tenant = brokered?.tenant ?? String(config.revenexxTenant || "");
143
+ const apiKey = String(config.revenexxApiKey || "");
144
+
145
+ if (!tenant || (!brokered?.jwt && !apiKey)) {
146
+ throw new RevenexxException(
147
+ "revenexx API credentials are not configured (NUXT_REVENEXX_TENANT / NUXT_REVENEXX_API_KEY)",
148
+ 503,
149
+ "api_not_configured",
150
+ "",
151
+ );
152
+ }
153
+
154
+ const client = new Client()
155
+ .setEndpoint(endpoint)
156
+ .setTenant(tenant);
157
+ if (brokered?.jwt) {
158
+ client.setBearerAuth(brokered.jwt);
159
+ }
160
+ else {
161
+ client.setApiKeyAuth(apiKey);
162
+ }
163
+
164
+ async function call<T>(
165
+ method: "GET" | "POST" | "PUT" | "DELETE",
166
+ path: string,
167
+ options: RevenexxSdkCallOptions = {},
168
+ ): Promise<T> {
169
+ const url = new URL(endpoint + path);
170
+ const query: Record<string, string> = {};
171
+ for (const [key, value] of Object.entries(options.query ?? {})) {
172
+ if (value !== undefined) {
173
+ query[key] = String(value);
174
+ }
175
+ }
176
+
177
+ if (method === "GET") {
178
+ // client.call appends flattened params to the URL for GET requests
179
+ return await client.call("get", url, {}, query) as T;
180
+ }
181
+
182
+ for (const [key, value] of Object.entries(query)) {
183
+ url.searchParams.set(key, value);
184
+ }
185
+ return await client.call(
186
+ method.toLowerCase(),
187
+ url,
188
+ { "content-type": "application/json" },
189
+ (options.body ?? {}) as never,
190
+ ) as T;
191
+ }
192
+
193
+ const instance: RevenexxSdk = {
194
+ client,
195
+ carts: new Carts(client),
196
+ customers: new Customers(client),
197
+ inventories: new Inventories(client),
198
+ markets: new Markets(client),
199
+ orders: new Orders(client),
200
+ payments: new Payments(client),
201
+ prices: new Prices(client),
202
+ products: new Products(client),
203
+ search: new Search(client),
204
+ shipping: new Shipping(client),
205
+ call,
206
+ };
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;
213
+ }
@@ -1,18 +0,0 @@
1
- import type { SimpleValidator } from "./types";
2
-
3
- // checks if the input is a valid company name (basic validation)
4
- export const companyName: SimpleValidator = (value) => {
5
- const companyNameRegex = /^[a-zA-Z0-9äöüÄÖÜß\s.,'-]+$/;
6
-
7
- if (typeof value !== "string") {
8
- return null; // Not a string, let other validators handle this
9
- }
10
-
11
- const trimmedValue = value.trim();
12
-
13
- if (!companyNameRegex.test(trimmedValue)) {
14
- return "invalidCompanyName"; // Invalid company name format
15
- }
16
-
17
- return null; // Valid company name
18
- };
@@ -1,12 +0,0 @@
1
- import type { SimpleValidator } from "./types";
2
-
3
- export const maxLength: SimpleValidator = (value, options) => {
4
- const max = (options?.max as number) ?? Infinity;
5
- if (typeof value !== "string") {
6
- return null;
7
- }
8
- if (value.trim().length > max) {
9
- return "maxLength";
10
- }
11
- return null;
12
- };
@@ -1,23 +0,0 @@
1
- import type { SimpleValidator } from "./types";
2
-
3
- // Validates company name only when a value is present — field is optional.
4
- // Options { min, max } are forwarded to minLength/maxLength translation keys.
5
- export const optionalCompanyName: SimpleValidator = (value) => {
6
- if (typeof value !== "string" || value.trim() === "") {
7
- return null;
8
- }
9
- const trimmed = value.trim();
10
- const min = 3;
11
- const max = 64;
12
- if (trimmed.length < min) {
13
- return "minLength";
14
- }
15
- if (trimmed.length > max) {
16
- return "maxLength";
17
- }
18
- const companyNameRegex = /^[a-zA-Z0-9äöüÄÖÜß\s.,'-]+$/;
19
- if (!companyNameRegex.test(trimmed)) {
20
- return "invalidCompanyName";
21
- }
22
- return null;
23
- };
@@ -1,19 +0,0 @@
1
- import type { SimpleValidator } from "./types";
2
-
3
- // checks if the input is a valid phone number (basic validation)
4
- export const phoneNumber: SimpleValidator = (value) => {
5
- if (typeof value !== "string") {
6
- return null; // Not a string, let other validators handle this
7
- }
8
-
9
- const trimmedValue = value.trim();
10
-
11
- // Basic regex for phone numbers (allows digits, spaces, dashes, parentheses, and plus sign)
12
- const phoneRegex = /^[+\d]?(?:[\d\s-().]*)$/;
13
-
14
- if (!phoneRegex.test(trimmedValue)) {
15
- return "invalidPhoneNumber"; // Invalid phone number format
16
- }
17
-
18
- return null; // Valid phone number
19
- };
@@ -1,4 +0,0 @@
1
- import type { SimpleValidator } from "./types";
2
-
3
- export const termsRequired: SimpleValidator = value =>
4
- value !== true ? "termsRequired" : null;
@@ -1,15 +0,0 @@
1
- import type { SimpleValidator } from "./types";
2
-
3
- export const zipCode: SimpleValidator = (value) => {
4
- if (typeof value !== "string") {
5
- return "invalidZipCode";
6
- }
7
- const trimmed = value.trim();
8
- if (!/^\d{3,5}$/.test(trimmed)) {
9
- return "invalidZipCode";
10
- }
11
- if (/^0+$/.test(trimmed) || trimmed === "99999") {
12
- return "invalidZipCode";
13
- }
14
- return null;
15
- };
@@ -1,56 +0,0 @@
1
- import type { H3Event } from "h3";
2
-
3
- import type { StoredSession } from "../../app/interfaces/auth";
4
- import type { AccountUser, IAccountService } from "../interfaces/account";
5
-
6
- function createInitials(name: string): string {
7
- const parts = name
8
- .trim()
9
- .split(/\s+/)
10
- .filter(Boolean);
11
- if (parts.length === 0) {
12
- return "";
13
- }
14
- return parts
15
- .slice(0, 2)
16
- .map(part => part[0]?.toUpperCase() ?? "")
17
- .join("");
18
- }
19
-
20
- /**
21
- * Live identity implementation backed by the revenexx web SDK.
22
- * Resolves the current user from the request's session cookie and the
23
- * platform account endpoint. Requires the NUXT_WEB_SDK_* runtime config.
24
- * Register via app.config → accountService: "sdk".
25
- */
26
- export class SdkAccountService implements IAccountService {
27
- async getUser(event?: H3Event): Promise<AccountUser> {
28
- if (!event) {
29
- throw createError({ statusCode: 500, statusMessage: "SdkAccountService requires the request event" });
30
- }
31
-
32
- const rawSession = getCookie(event, SESSION_COOKIE_NAME);
33
- if (!rawSession) {
34
- throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
35
- }
36
-
37
- let session: StoredSession;
38
- try {
39
- session = JSON.parse(rawSession) as StoredSession;
40
- }
41
- catch {
42
- throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
43
- }
44
- if (!session.fallbackCookie) {
45
- throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
46
- }
47
-
48
- const accountUser = await useAuthenticatedAccount(session.fallbackCookie).accountGet();
49
- return {
50
- id: accountUser.$id,
51
- name: accountUser.name,
52
- email: accountUser.email,
53
- initials: createInitials(accountUser.name),
54
- };
55
- }
56
- }
@@ -1,83 +0,0 @@
1
- import type { H3Event } from "h3";
2
-
3
- import type { AuthUser, StoredSession } from "../../app/interfaces/auth";
4
- import type { AuthLoginResult, IAuthService } from "../interfaces/auth";
5
-
6
- /**
7
- * Live authentication via the revenexx web SDK. Session state is carried in
8
- * the same cookie as the mock implementation (with fallbackCookie set), so
9
- * the client-side flow is identical. Requires NUXT_WEB_SDK_* runtime config.
10
- */
11
- export class SdkAuthService implements IAuthService {
12
- async login(event: H3Event, email: string, password: string): Promise<AuthLoginResult> {
13
- const { result: session, fallbackCookie } = await withCookieCapture(
14
- () => useShopSdkAccount().accountCreateEmailPasswordSession({ email, password }),
15
- );
16
-
17
- const user = await useAuthenticatedAccount(fallbackCookie).accountGet();
18
-
19
- const storedSession: StoredSession = {
20
- id: session.$id,
21
- expire: session.expire,
22
- fallbackCookie,
23
- };
24
- setCookie(event, SESSION_COOKIE_NAME, JSON.stringify(storedSession), sessionCookieOptions(session.expire));
25
-
26
- return {
27
- user: { $id: user.$id, name: user.name, email: user.email },
28
- session: { id: session.$id, expire: session.expire },
29
- };
30
- }
31
-
32
- async me(event: H3Event): Promise<AuthUser | null> {
33
- const raw = getCookie(event, SESSION_COOKIE_NAME);
34
- if (!raw) {
35
- return null;
36
- }
37
-
38
- let parsed: StoredSession;
39
- try {
40
- parsed = JSON.parse(raw) as StoredSession;
41
- }
42
- catch (err) {
43
- getLogService().error("Failed to parse session cookie", toErrorContext(err));
44
- deleteCookie(event, SESSION_COOKIE_NAME, sessionCookieOptions());
45
- return null;
46
- }
47
-
48
- if (!parsed.fallbackCookie) {
49
- deleteCookie(event, SESSION_COOKIE_NAME, sessionCookieOptions());
50
- return null;
51
- }
52
-
53
- try {
54
- const user = await useAuthenticatedAccount(parsed.fallbackCookie).accountGet();
55
- return { $id: user.$id, name: user.name, email: user.email };
56
- }
57
- catch (err) {
58
- if (!isSdkUserError(err)) {
59
- getLogService().error("SDK request failed: me/accountGet", sdkErrorContext(err));
60
- }
61
- deleteCookie(event, SESSION_COOKIE_NAME, sessionCookieOptions());
62
- return null;
63
- }
64
- }
65
-
66
- async logout(event: H3Event): Promise<void> {
67
- const raw = getCookie(event, SESSION_COOKIE_NAME);
68
- if (raw) {
69
- try {
70
- const parsed = JSON.parse(raw) as StoredSession;
71
- if (parsed.fallbackCookie) {
72
- const sessionId = parsed.id || "current";
73
- await useAuthenticatedAccount(parsed.fallbackCookie).accountDeleteSession({ sessionId });
74
- }
75
- }
76
- catch (err) {
77
- getLogService().error("SDK request failed: logout", sdkErrorContext(err));
78
- // always clear the cookie regardless of SDK result
79
- }
80
- }
81
- deleteCookie(event, SESSION_COOKIE_NAME, sessionCookieOptions());
82
- }
83
- }
@@ -1,136 +0,0 @@
1
- /**
2
- * Mini client for the public revenexx gateway (api.revenexx.com) — the
3
- * stand-in for the generated web SDKs that do not exist yet. Every live
4
- * "api"-keyed BFF implementation goes through this client; once the real
5
- * SDKs land, only the service implementations swap, the BFF contracts stay.
6
- *
7
- * Auth model: tenant API key (server-side only, never reaches the client).
8
- * Configure via runtime config / env:
9
- * NUXT_REVENEXX_API_URL (default https://api.revenexx.com)
10
- * NUXT_REVENEXX_TENANT (tenant slug, e.g. "revenexx")
11
- * NUXT_REVENEXX_API_KEY (rvxk_… gateway key)
12
- */
13
-
14
- export class RevenexxApiError extends Error {
15
- readonly statusCode: number;
16
- readonly type: string;
17
-
18
- constructor(statusCode: number, type: string, message: string) {
19
- super(message);
20
- this.name = "RevenexxApiError";
21
- this.statusCode = statusCode;
22
- this.type = type;
23
- }
24
- }
25
-
26
- /**
27
- * Expected user-facing failures (wrong credentials, duplicate email, …) are
28
- * forwarded to the client as-is; everything else is an infrastructure error.
29
- */
30
- const API_USER_ERROR_STATUS = new Set([400, 401, 404, 409, 429]);
31
-
32
- export function isApiUserError(err: unknown): err is RevenexxApiError {
33
- return err instanceof RevenexxApiError && API_USER_ERROR_STATUS.has(err.statusCode);
34
- }
35
-
36
- /**
37
- * The customers app's auth passthrough answers with a generic error envelope
38
- * (no machine type). Map the HTTP status onto the platform error types the
39
- * client-side auth store already translates, so mock/sdk/api modes produce
40
- * identical UX for the standard failures.
41
- */
42
- export function apiAuthErrorType(err: RevenexxApiError): string {
43
- switch (err.statusCode) {
44
- case 401: return "user_invalid_credentials";
45
- case 404: return "user_not_found";
46
- case 409: return "user_email_already_exists";
47
- case 429: return "general_rate_limit_exceeded";
48
- default: return err.type;
49
- }
50
- }
51
-
52
- /** Diagnostic fields for structured logs — never includes the API key. */
53
- export function apiErrorContext(err: unknown): Record<string, unknown> {
54
- if (err instanceof RevenexxApiError) {
55
- return { apiStatus: err.statusCode, apiType: err.type, apiMessage: err.message };
56
- }
57
- return { errorMessage: err instanceof Error ? err.message : String(err) };
58
- }
59
-
60
- export interface RevenexxApiRequest {
61
- readonly method?: "GET" | "POST" | "PUT" | "DELETE";
62
- readonly query?: Record<string, string | number | boolean | undefined>;
63
- readonly body?: unknown;
64
- }
65
-
66
- export interface RevenexxApiClient {
67
- request<T>(path: string, options?: RevenexxApiRequest): Promise<T>;
68
- get<T>(path: string, query?: RevenexxApiRequest["query"]): Promise<T>;
69
- post<T>(path: string, body?: unknown): Promise<T>;
70
- put<T>(path: string, body?: unknown): Promise<T>;
71
- }
72
-
73
- let client: RevenexxApiClient | null = null;
74
-
75
- export function useRevenexxApi(): RevenexxApiClient {
76
- if (client) {
77
- return client;
78
- }
79
-
80
- const config = useRuntimeConfig();
81
- const baseUrl = String(config.revenexxApiUrl || "https://api.revenexx.com").replace(/\/+$/, "");
82
- const tenant = String(config.revenexxTenant || "");
83
- const apiKey = String(config.revenexxApiKey || "");
84
-
85
- async function request<T>(path: string, options: RevenexxApiRequest = {}): Promise<T> {
86
- if (!tenant || !apiKey) {
87
- throw new RevenexxApiError(503, "api_not_configured", "revenexx API credentials are not configured (NUXT_REVENEXX_TENANT / NUXT_REVENEXX_API_KEY)");
88
- }
89
-
90
- const url = new URL(baseUrl + path);
91
- for (const [key, value] of Object.entries(options.query ?? {})) {
92
- if (value !== undefined) {
93
- url.searchParams.set(key, String(value));
94
- }
95
- }
96
-
97
- const response = await fetch(url, {
98
- method: options.method ?? "GET",
99
- headers: {
100
- "X-Revenexx-Tenant": tenant,
101
- "X-Revenexx-Api-Key": apiKey,
102
- "Accept": "application/json",
103
- ...(options.body !== undefined ? { "Content-Type": "application/json" } : {}),
104
- },
105
- ...(options.body !== undefined ? { body: JSON.stringify(options.body) } : {}),
106
- });
107
-
108
- const text = await response.text();
109
- let payload: unknown = null;
110
- try {
111
- payload = text ? JSON.parse(text) : null;
112
- }
113
- catch {
114
- payload = null;
115
- }
116
-
117
- if (!response.ok) {
118
- const envelope = (payload ?? {}) as { type?: string; message?: string; error?: string };
119
- throw new RevenexxApiError(
120
- response.status,
121
- envelope.type ?? "api_error",
122
- envelope.message ?? envelope.error ?? `revenexx API responded with ${response.status}`,
123
- );
124
- }
125
-
126
- return payload as T;
127
- }
128
-
129
- client = {
130
- request,
131
- get: (path, query) => request(path, { query }),
132
- post: (path, body) => request(path, { method: "POST", body }),
133
- put: (path, body) => request(path, { method: "PUT", body }),
134
- };
135
- return client;
136
- }
@@ -1,88 +0,0 @@
1
- import { Account, Client, RevenexxException } from "@revenexx/sdk";
2
-
3
- let baseClient: Client | null = null;
4
-
5
- function getBaseClient(): Client {
6
- if (baseClient) {
7
- return baseClient;
8
- }
9
- const config = useRuntimeConfig();
10
- baseClient = new Client();
11
- baseClient
12
- .setEndpoint(config.webSdkApiUrl as string)
13
- .setProject(config.webSdkProject as string)
14
- .setDevKey(config.webSdkDevKey as string);
15
- return baseClient;
16
- }
17
-
18
- export function useShopSdkAccount(): Account {
19
- return new Account(getBaseClient());
20
- }
21
-
22
- export function useAuthenticatedAccount(fallbackCookie: string): Account {
23
- const config = useRuntimeConfig();
24
- const client = new Client();
25
- client
26
- .setEndpoint(config.webSdkApiUrl as string)
27
- .setProject(config.webSdkProject as string)
28
- .setDevKey(config.webSdkDevKey as string);
29
- client.headers["X-Fallback-Cookies"] = fallbackCookie;
30
- return new Account(client);
31
- }
32
-
33
- const SDK_USER_ERROR_TYPES = new Set([
34
- "user_invalid_credentials",
35
- "user_not_found",
36
- "user_blocked",
37
- "general_rate_limit_exceeded",
38
- "user_session_already_exists",
39
- "user_email_already_exists",
40
- "user_invalid_token",
41
- "general_argument_invalid",
42
- "password_recently_used",
43
- ]);
44
-
45
- /**
46
- * Returns true for expected user-facing errors (wrong credentials, rate limit, etc.)
47
- * that should be forwarded to the client as-is without server-side logging.
48
- * Everything else is treated as an infrastructure error.
49
- */
50
- export function isSdkUserError(err: unknown): err is RevenexxException {
51
- return err instanceof RevenexxException && SDK_USER_ERROR_TYPES.has(err.type);
52
- }
53
-
54
- /**
55
- * Extracts diagnostic fields from an SDK error for structured log entries.
56
- * Does not include secrets (API key is never logged).
57
- */
58
- export function sdkErrorContext(err: unknown): Record<string, unknown> {
59
- if (err instanceof RevenexxException) {
60
- return { sdkCode: err.code, sdkType: err.type, sdkMessage: err.message };
61
- }
62
- return { errorMessage: err instanceof Error ? err.message : String(err) };
63
- }
64
-
65
- export async function withCookieCapture<T>(
66
- fn: () => Promise<T>,
67
- ): Promise<{ result: T; fallbackCookie: string }> {
68
- let fallbackCookie = "";
69
- const origFetch = globalThis.fetch;
70
- globalThis.fetch = async (
71
- input: RequestInfo | URL,
72
- init?: RequestInit,
73
- ) => {
74
- const res = await origFetch(input, init);
75
- const header = res.headers.get("x-fallback-cookies");
76
- if (header) {
77
- fallbackCookie = header;
78
- }
79
- return res;
80
- };
81
- try {
82
- const result = await fn();
83
- return { result, fallbackCookie };
84
- }
85
- finally {
86
- globalThis.fetch = origFetch;
87
- }
88
- }