@merkaly/nuxt 0.13.4 → 0.14.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.
package/README.md CHANGED
@@ -169,6 +169,21 @@ To open the Auth0 signup screen instead of the login one, use `useAuth().signup(
169
169
 
170
170
  The SPA keeps the session with **refresh tokens**: the client uses `useRefreshTokens` with `localStorage` cache and logs in with `audience` and `scope: openid profile email offline_access`. Before each API call the module asks Auth0 for the current access token, which is cached and renewed with the refresh token once it expires (2 hours for `https://api.merkaly.io`). When the refresh token is gone or expired, protected surfaces (`requiresAuth: true`) go back to login instead of calling the API without a token. Account linking (`linkWithConnection(connection)`) uses a separate Auth0 client, the `auth.merkaly.io` SPA client in Auth0, with an in-memory cache, created only on the first call (no extra `checkSession` on page load). It opens a login popup for that connection and sends the popup's raw ID token to `POST /auth/identities` as `{ idToken }`; the api verifies the token (signature, issuer, audience = the account linking client, expiry) and links the identity from its `sub` into the logged-in user. Requires an api with that contract (older clients that sent `{ provider, userId }` get `400`).
171
171
 
172
+ ### Expired session (`MKAuthExpired`)
173
+
174
+ When the session ends while the app is open (refresh token revoked, rotated or expired), the module does not log the user out and does not call the API with a dead token:
175
+
176
+ - `getTokenSilently` failing with a "no session" error (`login_required`, `invalid_grant`, `missing_refresh_token`, `consent_required`, `interaction_required`) after the user had a token calls `useAuth().expire()`: it clears `token` and sets `isExpired`.
177
+ - An API `401` for a signed-in user renews the token once (`getTokenSilently({ cacheMode: 'off' })`) and repeats the request; the hooks of the first attempt do not run. If the repeated request gets `401` again (after boot), it calls `expire()` too. Boot failures still go to the `error` slot.
178
+ - `MKApp` mounts `MKAuthExpired` once: a modal that cannot be dismissed, with a single button that calls `useAuth().relogin()` (`loginWithRedirect` with `appState.target` = current path and query), so the user lands back on the same page. Texts in `en`, `es` and `pt` (local i18n block).
179
+ - One state (`auth:expired`), so many failing calls show a single modal. Anonymous visitors (store, www) never had a token, so they never see it.
180
+
181
+ Apps need nothing beyond rendering `MKApp`.
182
+
183
+ ### Calling app header
184
+
185
+ Every API request carries the app's Auth0 client id (`merkaly.auth0.client`) in the `x-merkaly-client` header: the `$api` plugin adds it to server-side calls and the `/api` proxy sets it on every forwarded request (overwriting whatever the browser sent). The api checks it against its list of Auth0 applications and, on authenticated requests, against the token `azp` (api doc: [`docs/conventions/surface-clients.md`](https://github.com/merkaly-io/api.merkaly.io/blob/master/docs/conventions/surface-clients.md)). An app without `merkaly.auth0.client` sends no header.
186
+
172
187
  The Auth0 side (tenant, API, app settings, refresh token rotation and lifetimes, organizations, and the checklist for a new app) is documented in the auth.merkaly.io repo: [`docs/tenant-setup.md`](https://github.com/merkaly-io/auth.merkaly.io/blob/master/docs/tenant-setup.md).
173
188
 
174
189
  ## SEO: robots and sitemap
@@ -339,3 +354,28 @@ setMoneyDefaults({
339
354
  - `MKInputMoney`: `v-model` in cents; props `currency`, `locale`. `prefix`, `decimal`,
340
355
  `thousands` and `placeholder` default to the resolved currency/locale and can still
341
356
  be passed explicitly.
357
+
358
+ ## Datagrid (`useDatagrid`, `MKTableDatagrid`)
359
+
360
+ `useDatagrid(options)` builds the reactive state that `MKTableDatagrid` renders.
361
+ `options` is a `MaybeRefOrGetter`: a plain object, a ref or a getter returning it.
362
+
363
+ With a getter (or ref) the `columns` are recomputed reactively, so titles from i18n
364
+ follow a locale change. The rest of the options (`items`, `limit`, `total`, `search`,
365
+ `row`, ...) are read once as initial values; the state (`items`, `page`, `loading`,
366
+ ...) is never reset by a recompute.
367
+
368
+ ```ts
369
+ const { t: $t } = useI18n();
370
+
371
+ const $datagrid = useDatagrid<OrderEntity>(() => ({
372
+ columns: {
373
+ number: { title: $t('order.number') }, // follows the locale
374
+ total: { title: $t('order.total'), class: 'text-end' },
375
+ },
376
+ limit: 20, // initial value only
377
+ }));
378
+ ```
379
+
380
+ A plain object still works and keeps the previous behavior (static columns). Without
381
+ `title` the header falls back to the column key in sentence case.
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.14.0"
6
6
  },
7
- "version": "0.13.4",
7
+ "version": "0.14.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
@@ -5,6 +5,7 @@ import { useAuth, useOrganization, watchOnce, addRouteMiddleware, useNuxtApp, us
5
5
  import AuthMiddleware from "../middleware/auth";
6
6
  import { useNavigation } from "../composables/useNavigation";
7
7
  import AuthError from "./auth/AuthError.vue";
8
+ import AuthExpired from "./auth/AuthExpired.vue";
8
9
  import { useColorMode } from "@vueuse/core";
9
10
  import { Notivue, NotificationProgress, Notification, pastelTheme } from "notivue";
10
11
  const $route = useRoute();
@@ -41,6 +42,8 @@ hook("page:finish", () => regenerate());
41
42
  <slot v-else>
42
43
  <NuxtPage />
43
44
  </slot>
45
+
46
+ <AuthExpired />
44
47
  </BApp>
45
48
  </main>
46
49
  </template>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,43 @@
1
+ <script setup>
2
+ import { ref, useAuth } from "#imports";
3
+ import { useI18n } from "vue-i18n";
4
+ const { isExpired, relogin } = useAuth();
5
+ const { t } = useI18n({ useScope: "local" });
6
+ const isRedirecting = ref(false);
7
+ function signIn() {
8
+ isRedirecting.value = true;
9
+ relogin().catch(() => isRedirecting.value = false);
10
+ }
11
+ </script>
12
+
13
+ <template>
14
+ <BModal :model-value="isExpired" :title="t('title')" centered no-close-on-backdrop no-close-on-esc no-header-close>
15
+ <p class="mb-0" v-text="t('message')" />
16
+
17
+ <template #footer>
18
+ <BButton :loading="isRedirecting" variant="primary" @click="signIn">
19
+ <span v-text="t('signIn')" />
20
+ </BButton>
21
+ </template>
22
+ </BModal>
23
+ </template>
24
+
25
+ <i18n lang="json">
26
+ {
27
+ "en": {
28
+ "message": "For your security, sign in again to continue where you left off.",
29
+ "signIn": "Sign in again",
30
+ "title": "Your session expired"
31
+ },
32
+ "es": {
33
+ "message": "Por tu seguridad, vuelve a entrar para continuar donde estabas.",
34
+ "signIn": "Entrar de nuevo",
35
+ "title": "Tu sesión expiró"
36
+ },
37
+ "pt": {
38
+ "message": "Por segurança, entre novamente para continuar de onde parou.",
39
+ "signIn": "Entrar novamente",
40
+ "title": "Sua sessão expirou"
41
+ }
42
+ }
43
+ </i18n>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -101,10 +101,10 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
101
101
  }>, {
102
102
  title: string;
103
103
  disabled: boolean;
104
- multiple: boolean;
105
104
  accept: string[];
106
105
  hint: string;
107
106
  maxSize: number;
107
+ multiple: boolean;
108
108
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
109
109
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
110
110
  declare const _default: typeof __VLS_export;
@@ -101,10 +101,10 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
101
101
  }>, {
102
102
  title: string;
103
103
  disabled: boolean;
104
- multiple: boolean;
105
104
  accept: string[];
106
105
  hint: string;
107
106
  maxSize: number;
107
+ multiple: boolean;
108
108
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
109
109
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
110
110
  declare const _default: typeof __VLS_export;
@@ -7,8 +7,8 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
7
7
  type: import("vue").PropType<string>;
8
8
  };
9
9
  }>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
- change: () => any;
11
10
  "update:modelValue": (value: string) => any;
11
+ change: () => any;
12
12
  }, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
13
13
  debounce: {
14
14
  type: NumberConstructor;
@@ -18,8 +18,8 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
18
18
  type: import("vue").PropType<string>;
19
19
  };
20
20
  }>> & Readonly<{
21
- onChange?: (() => any) | undefined;
22
21
  "onUpdate:modelValue"?: ((value: string) => any) | undefined;
22
+ onChange?: (() => any) | undefined;
23
23
  }>, {
24
24
  debounce: number;
25
25
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -7,8 +7,8 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
7
7
  type: import("vue").PropType<string>;
8
8
  };
9
9
  }>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
- change: () => any;
11
10
  "update:modelValue": (value: string) => any;
11
+ change: () => any;
12
12
  }, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
13
13
  debounce: {
14
14
  type: NumberConstructor;
@@ -18,8 +18,8 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
18
18
  type: import("vue").PropType<string>;
19
19
  };
20
20
  }>> & Readonly<{
21
- onChange?: (() => any) | undefined;
22
21
  "onUpdate:modelValue"?: ((value: string) => any) | undefined;
22
+ onChange?: (() => any) | undefined;
23
23
  }>, {
24
24
  debounce: number;
25
25
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -16,10 +16,13 @@ export declare const useAuth: () => {
16
16
  claims: import("vue").Ref<User | undefined, User | undefined>;
17
17
  clear: () => undefined;
18
18
  error: import("vue").Ref<AuthError | undefined, AuthError | undefined>;
19
+ expire: () => void;
19
20
  fail: (reason: AuthError) => Promise<void>;
20
21
  isAuthenticated: import("vue").ComputedRef<boolean>;
22
+ isExpired: import("vue").Ref<boolean, boolean>;
21
23
  isLoading: import("vue").Ref<boolean, boolean>;
22
24
  logout: () => any;
25
+ relogin: () => any;
23
26
  retry: () => Promise<void>;
24
27
  signup: ({ redirectTo }?: {
25
28
  redirectTo?: string;
@@ -7,10 +7,16 @@ export const useAuth = () => {
7
7
  const token = useState("auth:token", () => null);
8
8
  const isLoading = useState("auth:loading", () => true);
9
9
  const error = useState("auth:error", () => void 0);
10
+ const isExpired = useState("auth:expired", () => false);
10
11
  const isAuthenticated = computed(() => Boolean(claims.value?.sub));
11
12
  const user = useBootstrapStage(nuxtApp, "user");
12
13
  const clear = () => error.value = void 0;
13
14
  const logout = () => nuxtApp.$auth0.logout();
15
+ const expire = () => {
16
+ token.value = null;
17
+ isExpired.value = true;
18
+ };
19
+ const relogin = () => nuxtApp.$auth0.loginWithRedirect({ appState: { target: location.pathname + location.search } });
14
20
  const signup = ({ redirectTo = "/" } = {}) => nuxtApp.$auth0.loginWithRedirect({
15
21
  appState: { target: redirectTo },
16
22
  authorizationParams: { screen_hint: "signup" }
@@ -33,10 +39,13 @@ export const useAuth = () => {
33
39
  claims,
34
40
  clear,
35
41
  error,
42
+ expire,
36
43
  fail,
37
44
  isAuthenticated,
45
+ isExpired,
38
46
  isLoading,
39
47
  logout,
48
+ relogin,
40
49
  retry,
41
50
  signup,
42
51
  token,
@@ -1,3 +1,4 @@
1
+ import type { MaybeRefOrGetter } from 'vue';
1
2
  export interface ColumnDefinition<C = unknown> {
2
3
  class?: string;
3
4
  getter?: (row: C) => unknown;
@@ -44,5 +45,5 @@ interface OptionArgs<D> {
44
45
  search?: string;
45
46
  total?: number;
46
47
  }
47
- export declare function useDatagrid<D = unknown>(params: OptionArgs<D>): DataGrid<D>;
48
+ export declare function useDatagrid<D = unknown>(options: MaybeRefOrGetter<OptionArgs<D>>): DataGrid<D>;
48
49
  export {};
@@ -1,11 +1,14 @@
1
- import { reactive } from "vue";
2
- export function useDatagrid(params) {
1
+ import { computed, isRef, reactive, toValue } from "vue";
2
+ export function useDatagrid(options) {
3
+ const params = toValue(options);
4
+ const isDynamic = typeof options === "function" || isRef(options);
5
+ const columns = isDynamic ? computed(() => toValue(options).columns) : params.columns;
3
6
  const state = reactive({
4
- columns: params.columns,
7
+ columns,
5
8
  error: params.error ?? null,
6
9
  fn: {
7
- addItem(item, options = {}) {
8
- const mode = options.mode ?? "append";
10
+ addItem(item, options2 = {}) {
11
+ const mode = options2.mode ?? "append";
9
12
  if (mode === "prepend") {
10
13
  state.items.unshift(item);
11
14
  } else if (mode === "append") {
@@ -1,18 +1,22 @@
1
1
  import { defineNuxtPlugin, useNuxtApp, useRequestURL, useRuntimeConfig } from "#app";
2
2
  import { useAuth, useOrganization } from "#imports";
3
3
  const GLOBAL_API_HEADER = "x-merkaly-global";
4
+ const CLIENT_HEADER = "x-merkaly-client";
4
5
  function serverBaseURL(api, global) {
5
6
  const prefix = global ? "" : (api.prefix ?? "").replace(/^\/+|\/+$/g, "");
6
7
  return prefix ? `${api.url.replace(/\/+$/, "")}/${prefix}` : api.url;
7
8
  }
8
9
  export default defineNuxtPlugin(({ provide }) => {
9
- const requestHost = import.meta.server ? useRequestURL().host : void 0;
10
+ const requestHost = import.meta.server ? useRequestURL({ xForwardedHost: true }).host : void 0;
10
11
  if (import.meta.server && import.meta.dev && new URL(useRuntimeConfig().public.merkaly.api.url).hostname.endsWith(".test")) {
11
12
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
12
13
  }
13
14
  const nuxtApp = useNuxtApp();
14
- provide("api", async (url, options = {}) => {
15
- const { token } = useAuth();
15
+ const renewed = /* @__PURE__ */ new WeakSet();
16
+ const api = async (url, options = {}) => {
17
+ const { claims, expire, isExpired, isLoading, token } = useAuth();
18
+ const canRenew = import.meta.client && Boolean(claims.value) && !isExpired.value && !renewed.has(options);
19
+ let unauthorized = false;
16
20
  if (import.meta.client && token.value) await nuxtApp.$auth0?.getTokenSilently();
17
21
  const { id: tenant } = useOrganization();
18
22
  const { public: { merkaly } } = useRuntimeConfig();
@@ -26,6 +30,7 @@ This surface has requiresTenant: true. Possible causes:
26
30
  3. The adapter should use { global: true } to bypass tenant enforcement.`
27
31
  );
28
32
  }
33
+ let result;
29
34
  await $fetch(url, {
30
35
  baseURL: import.meta.server ? serverBaseURL(merkaly.api, options.global) : "/api",
31
36
  body: options?.body,
@@ -35,6 +40,7 @@ This surface has requiresTenant: true. Possible causes:
35
40
  [GLOBAL_API_HEADER]: options.global ? "true" : "",
36
41
  ...requestHost ? { "x-merkaly-host": requestHost } : {},
37
42
  // tenant host, as the proxy sends it
43
+ ...merkaly.auth0.client ? { [CLIENT_HEADER]: merkaly.auth0.client } : {},
38
44
  ...options?.headers
39
45
  },
40
46
  method: options?.method,
@@ -43,8 +49,8 @@ This surface has requiresTenant: true. Possible causes:
43
49
  if (options.meta) options.meta.value = {};
44
50
  if (options.error) options.error.value = void 0;
45
51
  if (options.loading) options.loading.value = true;
46
- const result = await Promise.resolve(options?.onBeforeSend?.({ body: options.body, headers: options.headers, query: options.query })).then((res) => res).catch(() => false);
47
- if (result === false) {
52
+ const result2 = await Promise.resolve(options?.onBeforeSend?.({ body: options.body, headers: options.headers, query: options.query })).then((res) => res).catch(() => false);
53
+ if (result2 === false) {
48
54
  options.controller?.abort?.("Request aborted by onBeforeSend");
49
55
  }
50
56
  },
@@ -57,8 +63,11 @@ This surface has requiresTenant: true. Possible causes:
57
63
  onResponse: async ({ response, request }) => {
58
64
  const { status, _data, headers } = response;
59
65
  const { data, meta } = _data || {};
66
+ unauthorized = status === 401 && canRenew;
67
+ if (unauthorized) return;
60
68
  await options?.onResponse?.({ response, request });
61
69
  if (status >= 400) return;
70
+ result = data;
62
71
  await options?.onSuccess?.({ data, meta, headers, params: options.params });
63
72
  if (options.data) options.data.value = data;
64
73
  if (options.meta) options.meta.value = meta;
@@ -66,7 +75,9 @@ This surface has requiresTenant: true. Possible causes:
66
75
  if (options.loading) options.loading.value = false;
67
76
  },
68
77
  onResponseError: async ({ response, request }) => {
69
- const { _data } = response;
78
+ const { _data, status } = response;
79
+ if (unauthorized) return;
80
+ if (status === 401 && renewed.has(options) && !isLoading.value) expire();
70
81
  await options.onError?.(_data, { response, request });
71
82
  if (options.error) options.error.value = _data;
72
83
  await options?.onComplete?.({ response, request });
@@ -75,6 +86,11 @@ This surface has requiresTenant: true. Possible causes:
75
86
  query: options?.query,
76
87
  retry: false,
77
88
  signal: options.controller?.signal
78
- }).catch((reason) => reason);
79
- });
89
+ }).catch(() => void 0);
90
+ if (!unauthorized) return result;
91
+ const retry = { ...options };
92
+ renewed.add(retry);
93
+ return nuxtApp.$auth0.getTokenSilently({ cacheMode: "off" }).then(() => api(url, retry));
94
+ };
95
+ provide("api", api);
80
96
  });
@@ -15,7 +15,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
15
15
  domain: $config.merkaly.auth0.domain,
16
16
  useRefreshTokens: true
17
17
  });
18
- const { claims, error, isLoading, token, fail } = useAuth();
18
+ const { claims, error, expire, isLoading, token, fail } = useAuth();
19
19
  const isStarting = useState("auth:starting", () => false);
20
20
  const self0 = Object.assign(Object.create(Object.getPrototypeOf(auth0)), auth0);
21
21
  let tokenError;
@@ -25,6 +25,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
25
25
  return token.value = result;
26
26
  }).catch((err) => {
27
27
  tokenError = err;
28
+ if (token.value && NO_SESSION_ERRORS.includes(err.error)) return expire();
28
29
  console.warn("[Auth0] getTokenSilently failed \u2013 fallback, user logged in?", err);
29
30
  });
30
31
  const resolveIdentity = () => Promise.allSettled([auth0.getUser(), auth0.getTokenSilently()]);
@@ -94,12 +95,19 @@ export default defineNuxtPlugin(async (nuxtApp) => {
94
95
  throw new Error("Failed to get ID token for linking");
95
96
  }
96
97
  const { $api } = useNuxtApp();
97
- return await $api("/auth/identities", {
98
+ let failure;
99
+ const linked = await $api("/auth/identities", {
98
100
  // the api verifies it and derives the identity
99
101
  body: { idToken: idClaims.__raw },
100
102
  global: true,
101
- method: "POST"
103
+ method: "POST",
104
+ onError: (reason) => void (failure = reason ?? {}),
105
+ // api error body
106
+ onFatal: (reason) => void (failure = reason ?? {})
107
+ // network or aborted
102
108
  });
109
+ if (failure) throw new Error(String(failure.message || "Failed to link the account"));
110
+ return linked;
103
111
  };
104
112
  const isAuthCallback = window.location.pathname === callbackPath;
105
113
  if (isAuthCallback) {
@@ -2,6 +2,7 @@ import { defineEventHandler, getRequestURL, proxyRequest } from "h3";
2
2
  import { useRuntimeConfig } from "#imports";
3
3
  const GLOBAL_API_HEADER = "x-merkaly-global";
4
4
  const TENANT_HOST_HEADER = "x-merkaly-host";
5
+ const CLIENT_HEADER = "x-merkaly-client";
5
6
  const API_PREFIX_PATTERN = /^\/api\/?/;
6
7
  const API_ROUTE_PATTERN = /^\/api(?:\/|$)/;
7
8
  const EDGE_SLASH_PATTERN = /^\/|\/$/g;
@@ -28,11 +29,13 @@ export default defineEventHandler((event) => {
28
29
  }
29
30
  const { public: $config } = useRuntimeConfig();
30
31
  const targetOrigin = new URL($config.merkaly.api.url);
31
- const url = getRequestURL(event);
32
+ const url = getRequestURL(event, { xForwardedHost: true });
32
33
  const prefix = isGlobalRequest(event.node.req.headers[GLOBAL_API_HEADER]) ? "" : $config.merkaly.api.prefix;
33
34
  const path = resolveProxyPath(prefix, url.pathname, url.search);
34
35
  event.node.req.headers["x-forwarded-host"] = url.host;
35
36
  event.node.req.headers[TENANT_HOST_HEADER] = url.host;
37
+ if ($config.merkaly.auth0.client) event.node.req.headers[CLIENT_HEADER] = $config.merkaly.auth0.client;
38
+ else Reflect.deleteProperty(event.node.req.headers, CLIENT_HEADER);
36
39
  stripEdgeHeaders(event.node.req.headers);
37
40
  if (import.meta.dev && targetOrigin.hostname.endsWith(".test")) {
38
41
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
@@ -10,7 +10,7 @@ type MerkalyAuth0Client = Auth0Client & {
10
10
  declare module '#app' {
11
11
  interface NuxtApp {
12
12
  $auth0: MerkalyAuth0Client;
13
- $api: <T = unknown>(url: string, options?: ApiOptions) => Promise<T>;
13
+ $api: <T = unknown>(url: string, options?: ApiOptions) => Promise<T | undefined>; // undefined when the request fails
14
14
  $gmap: never;
15
15
  }
16
16
 
@@ -25,7 +25,7 @@ declare module '#app' {
25
25
  declare module 'vue' {
26
26
  interface ComponentCustomProperties {
27
27
  $auth0: MerkalyAuth0Client;
28
- $api: <T = unknown>(url: string, options?: ApiOptions) => Promise<T>;
28
+ $api: <T = unknown>(url: string, options?: ApiOptions) => Promise<T | undefined>; // undefined when the request fails
29
29
  $gmap: never;
30
30
  }
31
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merkaly/nuxt",
3
- "version": "0.13.4",
3
+ "version": "0.14.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/merkaly-io/nuxt.git"
@@ -52,9 +52,11 @@
52
52
  "change-case": "^5.4.4",
53
53
  "class-transformer": "^0.5.1",
54
54
  "class-validator": "^0.15.1",
55
+ "defu": "^6.1.7",
55
56
  "eslint": "^10.14.0",
56
57
  "filesize": "^11.0.17",
57
58
  "gsap": "^3.15.0",
59
+ "jiti": "^2.7.0",
58
60
  "notivue": "^2.4.5",
59
61
  "nuxt": "^4.6.3",
60
62
  "posthog-js": "^1.447.1",
@@ -63,19 +65,16 @@
63
65
  "typescript": "~5.9.3",
64
66
  "v-money3": "^3.26.1",
65
67
  "vite-svg-loader": "^5.1.1",
66
- "vue": "^3.5.43",
68
+ "vue": "^3.5.45",
67
69
  "vue-router": "^5.4.0",
68
70
  "vue-select": "4.0.0-beta.6"
69
71
  },
70
72
  "devDependencies": {
71
73
  "@nuxt/module-builder": "^1.0.2",
72
- "@nuxt/schema": "^4.6.0",
74
+ "@nuxt/schema": "^4.6.4",
73
75
  "changelogen": "^0.6.2",
74
76
  "vue-tsc": "^3.3.4"
75
77
  },
76
- "optionalDependencies": {
77
- "defu": "^6.1.7"
78
- },
79
78
  "engines": {
80
79
  "node": ">=24"
81
80
  },