@merkaly/nuxt 0.12.5 → 0.13.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 (30) hide show
  1. package/README.md +122 -6
  2. package/dist/module.d.mts +1 -0
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +1 -0
  5. package/dist/runtime/components/app.vue +2 -2
  6. package/dist/runtime/components/auth/AuthError.vue +9 -2
  7. package/dist/runtime/components/format/FormatDate.d.vue.ts +2 -2
  8. package/dist/runtime/components/format/FormatDate.vue.d.ts +2 -2
  9. package/dist/runtime/components/format/FormatIcon.d.vue.ts +2 -2
  10. package/dist/runtime/components/format/FormatIcon.vue.d.ts +2 -2
  11. package/dist/runtime/composables/useAuth.d.ts +9 -2
  12. package/dist/runtime/composables/useAuth.js +5 -2
  13. package/dist/runtime/composables/useOrganization.d.ts +13 -0
  14. package/dist/runtime/composables/useOrganization.js +13 -0
  15. package/dist/runtime/internal/bootstrap.d.ts +2 -0
  16. package/dist/runtime/internal/bootstrap.js +13 -0
  17. package/dist/runtime/internal/state.d.ts +45 -0
  18. package/dist/runtime/internal/state.js +51 -0
  19. package/dist/runtime/plugins/api.global.js +3 -3
  20. package/dist/runtime/plugins/auth0.client.js +15 -12
  21. package/dist/runtime/plugins/bootstrap.server.d.ts +3 -0
  22. package/dist/runtime/plugins/bootstrap.server.js +6 -0
  23. package/dist/runtime/plugins/posthog.client.js +3 -3
  24. package/dist/runtime/types/nuxt.d.ts +2 -2
  25. package/dist/runtime/utils/defineBootstrap.d.ts +2 -0
  26. package/dist/runtime/utils/defineBootstrap.js +3 -0
  27. package/dist/types.d.mts +2 -0
  28. package/package.json +1 -1
  29. package/dist/runtime/composables/useTenant.d.ts +0 -8
  30. package/dist/runtime/composables/useTenant.js +0 -17
package/README.md CHANGED
@@ -26,14 +26,130 @@ Install the module to your Nuxt application:
26
26
 
27
27
  Every surface can sign in; `auth0.requiresAuth` only decides whether pages require a login (globally, or per page with `definePageMeta({ requiresAuth: true })`). `auth0.requiresTenant` decides whether API calls require a tenant (`global: true` adapters bypass it).
28
28
 
29
- On boot the module restores an existing session (user and token) and then runs the app hooks in order, `merkaly:tenant` then `merkaly:auth`, with that user (or none). `MKApp` shows its `loading` slot until both finish; if a hook throws, it shows the `error` slot instead. The auth callback page renders right away so it can complete the login.
29
+ On boot the module restores an existing session (Auth0 claims and token) and then runs the bootstrap stages in a fixed order, each one only after the previous one finished:
30
+
31
+ 1. **session**: the app hooks `merkaly:tenant` then `merkaly:auth`, with those claims (or none).
32
+ 2. **organization**: the app's organization loader (see `defineBootstrap`), always, signed in or not.
33
+ 3. **user**: the app's `/auth/me` loader, only when the visitor is signed in.
34
+
35
+ The app mounts (and `MKApp` renders its default slot) only after the last stage, so pages, layouts and their requests start with the session, the organization and the user already loaded. A failing stage records the failure with `useAuth().fail()` (stage `tenant`, `organization` or `user`; never a logout) and `MKApp` renders the `error` slot; `retry()` runs again only the stages that did not succeed. The auth callback page renders right away so it can complete the login, and runs the stages after it.
30
36
 
31
37
  | Surface | Login | Tenant | Boot |
32
38
  |---|---|---|---|
33
- | admin | required | required | `merkaly:tenant` creates the session and sets the tenant; without a session the auth middleware redirects to login. |
34
- | dashboard | required | not used | `merkaly:auth` loads the session; without a session it redirects to login. |
35
- | store | optional | required | `merkaly:tenant` resolves the store from the host (public); a signed-in buyer is restored too; protected pages (account, checkout) ask for login. |
36
- | www | optional | not used | No hooks; anonymous or signed-in. |
39
+ | admin | required | required | `merkaly:tenant` creates the session and sets the tenant; then organization (`/settings/organization`) and user (`/auth/me`). Without a session the auth middleware redirects to login. |
40
+ | dashboard | required | not used | `merkaly:auth` loads the session; then user if it declares a loader. Without a session it redirects to login. |
41
+ | store | optional | required | `merkaly:tenant` resolves the store from the host (public); then organization (`/company`, public, also during SSR); user only for a signed-in buyer. Protected pages (account, checkout) ask for login. |
42
+ | www | optional | not used | No hooks and no loaders; anonymous or signed-in. |
43
+
44
+ ### Ordered bootstrap (`defineBootstrap`, `useOrganization`, `useAuth().user`)
45
+
46
+ Each app declares its loaders once, from a plugin, with its own adapters (the module has no app endpoints):
47
+
48
+ ```ts
49
+ // admin: app/plugins/bootstrap.ts
50
+ import { readAccountProfile } from '~/adapters/account/read.account.profile';
51
+ import { readSettingsOrganization } from '~/adapters/settings/organization/read.settings.organization';
52
+
53
+ export default defineNuxtPlugin(() => {
54
+ defineBootstrap({
55
+ organization: () => readSettingsOrganization(() => ({ immediate: false })),
56
+ user: () => readAccountProfile(() => ({ immediate: false })),
57
+ });
58
+ });
59
+ ```
60
+
61
+ ```ts
62
+ // store: app/plugins/bootstrap.ts (universal, not .client: SSR loads the organization too)
63
+ import { readAppCompany } from '~/adapters/app/read.company';
64
+
65
+ export default defineNuxtPlugin(() => {
66
+ defineBootstrap({ organization: () => readAppCompany(() => ({ immediate: false })) });
67
+ });
68
+ ```
69
+
70
+ A loader returns an adapter (`withAdapter()` result, preferably with `immediate: false`, the bootstrap runs it), a promise or a plain value. An adapter with an api error, or a rejected promise, fails the stage. Every stage is optional: an app without loaders skips it.
71
+
72
+ Two composables hold the state, shared by the whole app:
73
+
74
+ - `useOrganization()`: everything about the organization (tenant). It exists without a login and also loads on the server.
75
+ - `useAuth()`: everything about the user: `user`, the loaded profile (`/auth/me`), and `claims`, the Auth0 ID token user (`sub`, `email`, `name`, `picture`, custom claims), plus `token`, `isAuthenticated` (from `claims.sub`), `fail`, `retry`, `logout`, `signup`.
76
+
77
+ Use `claims` for what the token already carries (id, email, roles) and `user.data` for the profile the api stores (own avatar, locale, saved name).
78
+
79
+ | `useOrganization()` | Description |
80
+ |---|---|
81
+ | `id` | Organization id (Auth0 org), sent as the `identity` header; `''` until set |
82
+ | `setId(id, { server? })` | Sets the id; `server: true` when resolved during SSR |
83
+ | `hasId` | `Boolean(id)` |
84
+ | `fromServer` | The id was resolved during SSR: the page renders before auth |
85
+ | `data`, `status`, `error`, `isReady`, `refresh()` | The loaded organization, see below |
86
+
87
+ | `useAuth().user` and the stage keys of `useOrganization()` | Description |
88
+ |---|---|
89
+ | `data` | Loaded value; `undefined` until it loads (and always for `user` with visitors) |
90
+ | `status` | `idle`, `pending`, `success` or `error` |
91
+ | `error` | `{ message, status? }` of the last failure |
92
+ | `isReady` | `status === 'success'` |
93
+ | `refresh()` | Loads it again (e.g. after saving the settings); a failure here only sets `error`, it does not block the app |
94
+
95
+ ```ts
96
+ const { data: organization, id } = useOrganization();
97
+ const { claims, user } = useAuth();
98
+
99
+ const name = computed(() => user.data.value?.name || claims.value?.name); // typed by MerkalyUser
100
+ ```
101
+
102
+ #### Typing (`MerkalyOrganization`, `MerkalyUser`)
103
+
104
+ `data` is typed with two interfaces exported by the package, empty by default. Each app extends them once with its own contracts (module augmentation, no generics at call sites), e.g. in `app/types/merkaly.d.ts`:
105
+
106
+ ```ts
107
+ import type { AccountAuthProfile } from '@merkaly/api/account/auth';
108
+ import type { SettingsOrganizationProfileResponse } from '@merkaly/api/settings/organization';
109
+
110
+ declare module '@merkaly/nuxt' {
111
+ interface MerkalyOrganization extends SettingsOrganizationProfileResponse {}
112
+ interface MerkalyUser extends AccountAuthProfile {}
113
+ }
114
+ ```
115
+
116
+ The file must be a module (keep at least one `import` or an `export {}`), otherwise it replaces the package types instead of extending them. The loaders passed to `defineBootstrap` are checked against the same interfaces.
117
+
118
+ In SSR apps (store) the organization stage also runs on the server, after the server plugins (`tenant.server.ts`), so it reaches the HTML; the client reuses it from the payload and does not request it again. `user` never runs on the server (there is no session there), so avoid rendering `user` data in SSR markup to keep hydration stable.
119
+
120
+ #### Migration per app (breaking: `useTenant` is gone and `useAuth().user` changed)
121
+
122
+ - `useTenant()` is removed; its keys live in `useOrganization()`: `tenant` → `id`, `setTenant(id, options)` → `setId(id, options)`, `fromServer` → `fromServer`, `isReady` (id set) → `hasId` (`isReady` now means the organization data loaded). The `merkaly:tenant` hook is unchanged: it still runs first and is where the id gets set.
123
+ - `useAuth().user` (the Auth0 user) is now `useAuth().claims`, same object. `useAuth().user` is the loaded profile (`data`, `status`, `error`, `isReady`, `refresh`). Every current app read of `user` is a token field (name, email, picture, sub, roles claim), so the rename is `user` → `claims`, in templates too (`user.name` → `claims.name`, `v-if="user"` → `v-if="claims"`). `isAuthenticated` is unchanged.
124
+ - The `merkaly:tenant` and `merkaly:auth` hooks still receive the Auth0 claims.
125
+
126
+ - **admin**
127
+ - `app/plugins/auth.ts:4`: `const { setTenant } = useTenant()` → `const { setId } = useOrganization()`; line 25: `setTenant(data.orgId)` → `setId(data.orgId)`.
128
+ - `app/plugins/money.ts:8-18`: drop `useTenant()`, the `readSettingsOrganization()` call and the `watch(tenant, ...)`; use `watch(useOrganization().data, (data) => data && setMoneyDefaults({ currency: data.localization?.currency, locale: data.localization?.language }), { immediate: true })`.
129
+ - New `app/plugins/bootstrap.ts` and `app/types/merkaly.d.ts` (above).
130
+ - `app/composables/useAccountProfile.ts:5`: `const { user } = useAuth()` → `const { claims, user } = useAuth()`; line 7: `readAccountProfile()` → `const { data: profile, refresh } = user`; line 9: `user.value?.picture` → `claims.value?.picture`.
131
+ - `app/components/ui/select/SelectLocale.vue:9`: `{ user }` → `{ claims }`; line 32: `user.value?.name` → `claims.value?.name`.
132
+ - `app/components/ui/nav/NavUser.vue:2`: `{ user }` → `{ claims }`; template lines 22, 24, 25, 29, 30, 33, 50, 54, 61: `user` / `user.name` / `user.email` → `claims` / `claims.name` / `claims.email`.
133
+ - `app/pages/settings/(overview)/.components/SettingsMembersSection.vue:16`: `{ user }` → `{ claims }`; line 28: `user.value?.[AUTH_CLAIM]` → `claims.value?.[AUTH_CLAIM]`; line 36: `user.value?.sub` → `claims.value?.sub`.
134
+ - `app/pages/profile/(overview)/.components/ProfileOverviewSection.vue:7`: `{ user }` → `{ claims }`; lines 16, 22: `user.value?.name` → `claims.value?.name`; template lines 83 and 109: `user?.name`, `user?.email` → `claims?.name`, `claims?.email`.
135
+ - `app/pages/profile/(overview)/.components/ProfileSignInMethodSection.vue:9`: `{ user }` → `{ claims }`; template line 74: `user?.email` → `claims?.email`.
136
+ - `app/pages/settings/(overview)/.composables/useSettingsOrganization.ts:5` (optional): read `useOrganization()` (`data`, `status === 'pending'`, `refresh`) instead of a second `readSettingsOrganization()`.
137
+ - **store**
138
+ - `app/plugins/tenant.server.ts:4`: `const { setTenant } = useTenant()` → `const { setId } = useOrganization()`; lines 7 and 15: `setTenant(..., { server: true })` → `setId(..., { server: true })`.
139
+ - `app/plugins/auth.ts:4`: `const { fromServer, setTenant } = useTenant()` → `const { fromServer, setId } = useOrganization()`; lines 9 and 13: `setTenant(...)` → `setId(...)`.
140
+ - `app/pages/checkout/.composables/usePaymentStream.ts:11`: `const { tenant } = useTenant()` → `const { id: tenant } = useOrganization()` (line 43 keeps `tenant.value`).
141
+ - `app/components/nav/NavUser.vue:8`: `user` → `claims` in the destructuring; template line 55: `user?.name` → `claims?.name`.
142
+ - `app/pages/checkout/index.vue:22`: `{ user }` → `{ claims }`; line 76: `watch(user, ...)` → `watch(claims, ...)`.
143
+ - `app/pages/account/.components/AccountUser.vue:2`: `{ user }` → `{ claims }`; line 4 and template lines 9, 10, 21, 23, 24: `user` → `claims`.
144
+ - New universal `app/plugins/bootstrap.ts` (above) and `app/types/merkaly.d.ts` with `MerkalyOrganization extends SettingsOrganizationStoreProfile`.
145
+ - `app/layouts/default.vue:5` and `app/pages/account/orders/[order]/index.vue:85`: `readAppCompany()` → `const { data: company } = useOrganization()`.
146
+ - `app/plugins/README.md:11` mentions `fromServer`: now from `useOrganization()`.
147
+ - **dashboard**
148
+ - `app/components/ui/nav/NavUser.vue:2`: `{ user }` → `{ claims }`; line 7 and template lines 12, 21, 28, 33, 35: `user` → `claims`.
149
+ - No `useTenant` usages. Add `defineBootstrap({ user })` and `MerkalyUser` only if it needs `/auth/me` before the pages.
150
+ - **www**
151
+ - `app/components/modals/ModalBookDemo.vue:11`: `{ isAuthenticated, user }` → `{ isAuthenticated, claims }`; lines 14 and 15: `user.value?.name`, `user.value?.email` → `claims.value?.name`, `claims.value?.email`.
152
+ - No `useTenant` usages, no loaders needed.
37
153
 
38
154
  ### Login entry points (`MKAuth`)
39
155
 
@@ -103,7 +219,7 @@ overrides it at runtime.
103
219
  **Client plugin.** Initializes `posthog-js` **only in production builds**
104
220
  (`nuxt dev` never sends events) with `api_host: '/phog'` and
105
221
  `ui_host: 'https://us.posthog.com'`. It identifies the Auth0 user from
106
- `useAuth()` (`identify(sub, { email, name })`) and calls `posthog.reset()` on
222
+ `useAuth().claims` (`identify(sub, { email, name })`) and calls `posthog.reset()` on
107
223
  logout. The app can keep using `import posthog from 'posthog-js'` for
108
224
  `capture()`: the module dedupes `posthog-js` so the app and the plugin share
109
225
  the initialized instance (keep `posthog-js` as an app dependency).
package/dist/module.d.mts CHANGED
@@ -3,6 +3,7 @@ import { ClientAuthorizationParams } from '@auth0/auth0-spa-js';
3
3
  import { Strategies } from '@nuxtjs/i18n';
4
4
  export { AdapterArgs, AdapterOptions } from '../dist/runtime/utils/withAdapter.js';
5
5
  export { ApiOptions, HooksOptions, ParamsOptions, RefOptions } from '../dist/runtime/plugins/api.global.js';
6
+ export { BootstrapError, BootstrapLoader, BootstrapLoaders, BootstrapStatus, MerkalyOrganization, MerkalyUser } from '../dist/runtime/internal/state.js';
6
7
 
7
8
  interface MerkalyI18nLocale {
8
9
  code: string;
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.14.0"
6
6
  },
7
- "version": "0.12.5",
7
+ "version": "0.13.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -278,6 +278,7 @@ function configureNuxtOptions(nuxt, options) {
278
278
  function registerRuntimeFeatures(options, resolver) {
279
279
  addPlugin({ src: resolver.resolve("./runtime/plugins/api.global") });
280
280
  addPlugin({ src: resolver.resolve("./runtime/plugins/auth0.client") });
281
+ addPlugin({ src: resolver.resolve("./runtime/plugins/bootstrap.server") });
281
282
  addPlugin({ src: resolver.resolve("./runtime/plugins/sentry.global") });
282
283
  if (hasPosthogConfig(options)) {
283
284
  addPlugin({ src: resolver.resolve("./runtime/plugins/posthog.client") });
@@ -1,7 +1,7 @@
1
1
  <script setup>
2
2
  import { useRoute } from "#app";
3
3
  import { computed } from "vue";
4
- import { useAuth, useTenant, watchOnce, addRouteMiddleware, useNuxtApp, useRuntimeConfig } from "#imports";
4
+ import { useAuth, useOrganization, watchOnce, addRouteMiddleware, useNuxtApp, useRuntimeConfig } from "#imports";
5
5
  import AuthMiddleware from "../middleware/auth";
6
6
  import { useNavigation } from "../composables/useNavigation";
7
7
  import AuthError from "./auth/AuthError.vue";
@@ -9,7 +9,7 @@ import { useColorMode } from "@vueuse/core";
9
9
  import { Notivue, NotificationProgress, Notification, pastelTheme } from "notivue";
10
10
  const $route = useRoute();
11
11
  const { clear, error, isLoading, logout, retry } = useAuth();
12
- const { fromServer } = useTenant();
12
+ const { fromServer } = useOrganization();
13
13
  const { hook } = useNuxtApp();
14
14
  const { public: { merkaly } } = useRuntimeConfig();
15
15
  useColorMode({ attribute: "data-bs-theme" });
@@ -5,8 +5,15 @@ const props = defineProps({
5
5
  });
6
6
  const emit = defineEmits(["logout", "retry"]);
7
7
  const isDenied = computed(() => [401, 403].includes(props.error.status));
8
- const title = computed(() => isDenied.value ? "Access denied" : "We could not start your session");
9
- const description = computed(() => isDenied.value ? "You signed in successfully, but this account is not allowed to access this application." : "You signed in successfully, but something failed while preparing your session.");
8
+ const isOrganization = computed(() => props.error.stage === "organization");
9
+ const title = computed(() => {
10
+ if (isDenied.value) return "Access denied";
11
+ return isOrganization.value ? "We could not load this organization" : "We could not start your session";
12
+ });
13
+ const description = computed(() => {
14
+ if (isDenied.value) return "You signed in successfully, but this account is not allowed to access this application.";
15
+ return isOrganization.value ? "Something failed while loading the organization of this application." : "You signed in successfully, but something failed while preparing your session.";
16
+ });
10
17
  const details = computed(() => [props.error.stage, props.error.status, props.error.url].filter(Boolean).join(" \xB7 "));
11
18
  </script>
12
19
 
@@ -1,7 +1,7 @@
1
1
  import type { PropType } from 'vue';
2
2
  declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
3
3
  date: {
4
- type: (DateConstructor | StringConstructor)[];
4
+ type: (StringConstructor | DateConstructor)[];
5
5
  default: () => Date;
6
6
  };
7
7
  format: {
@@ -10,7 +10,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
10
10
  };
11
11
  }>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
12
12
  date: {
13
- type: (DateConstructor | StringConstructor)[];
13
+ type: (StringConstructor | DateConstructor)[];
14
14
  default: () => Date;
15
15
  };
16
16
  format: {
@@ -1,7 +1,7 @@
1
1
  import type { PropType } from 'vue';
2
2
  declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
3
3
  date: {
4
- type: (DateConstructor | StringConstructor)[];
4
+ type: (StringConstructor | DateConstructor)[];
5
5
  default: () => Date;
6
6
  };
7
7
  format: {
@@ -10,7 +10,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
10
10
  };
11
11
  }>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
12
12
  date: {
13
- type: (DateConstructor | StringConstructor)[];
13
+ type: (StringConstructor | DateConstructor)[];
14
14
  default: () => Date;
15
15
  };
16
16
  format: {
@@ -18,7 +18,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
18
18
  required: true;
19
19
  };
20
20
  opacity: {
21
- type: (NumberConstructor | StringConstructor)[];
21
+ type: (StringConstructor | NumberConstructor)[];
22
22
  default: () => string;
23
23
  };
24
24
  provider: {
@@ -71,7 +71,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
71
71
  required: true;
72
72
  };
73
73
  opacity: {
74
- type: (NumberConstructor | StringConstructor)[];
74
+ type: (StringConstructor | NumberConstructor)[];
75
75
  default: () => string;
76
76
  };
77
77
  provider: {
@@ -18,7 +18,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
18
18
  required: true;
19
19
  };
20
20
  opacity: {
21
- type: (NumberConstructor | StringConstructor)[];
21
+ type: (StringConstructor | NumberConstructor)[];
22
22
  default: () => string;
23
23
  };
24
24
  provider: {
@@ -71,7 +71,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
71
71
  required: true;
72
72
  };
73
73
  opacity: {
74
- type: (NumberConstructor | StringConstructor)[];
74
+ type: (StringConstructor | NumberConstructor)[];
75
75
  default: () => string;
76
76
  };
77
77
  provider: {
@@ -1,5 +1,5 @@
1
1
  import type { User } from '@auth0/auth0-spa-js';
2
- export type AuthErrorStage = 'callback' | 'token' | 'session' | 'tenant';
2
+ export type AuthErrorStage = 'callback' | 'token' | 'session' | 'tenant' | 'organization' | 'user';
3
3
  export interface AuthError {
4
4
  message: string;
5
5
  stage: AuthErrorStage;
@@ -13,6 +13,7 @@ export interface AuthErrorContext {
13
13
  retry: () => Promise<void>;
14
14
  }
15
15
  export declare const useAuth: () => {
16
+ claims: import("vue").Ref<User | undefined, User | undefined>;
16
17
  clear: () => undefined;
17
18
  error: import("vue").Ref<AuthError | undefined, AuthError | undefined>;
18
19
  fail: (reason: AuthError) => Promise<void>;
@@ -24,5 +25,11 @@ export declare const useAuth: () => {
24
25
  redirectTo?: string;
25
26
  }) => any;
26
27
  token: import("vue").Ref<string | null, string | null>;
27
- user: import("vue").Ref<User | undefined, User | undefined>;
28
+ user: {
29
+ data: import("vue").Ref<import("../internal/state.js").MerkalyUser | undefined, import("../internal/state.js").MerkalyUser | undefined>;
30
+ error: import("vue").Ref<import("../internal/state.js").BootstrapError | undefined, import("../internal/state.js").BootstrapError | undefined>;
31
+ isReady: import("vue").ComputedRef<boolean>;
32
+ refresh: () => Promise<boolean>;
33
+ status: import("vue").Ref<import("../internal/state.js").BootstrapStatus, import("../internal/state.js").BootstrapStatus>;
34
+ };
28
35
  };
@@ -1,12 +1,14 @@
1
1
  import { computed } from "vue";
2
2
  import { useNuxtApp, useState } from "#imports";
3
+ import { useBootstrapStage } from "../internal/state.js";
3
4
  export const useAuth = () => {
4
5
  const nuxtApp = useNuxtApp();
5
- const user = useState("auth:user", () => void 0);
6
+ const claims = useState("auth:claims", () => void 0);
6
7
  const token = useState("auth:token", () => null);
7
8
  const isLoading = useState("auth:loading", () => true);
8
9
  const error = useState("auth:error", () => void 0);
9
- const isAuthenticated = computed(() => Boolean(user.value?.sub));
10
+ const isAuthenticated = computed(() => Boolean(claims.value?.sub));
11
+ const user = useBootstrapStage(nuxtApp, "user");
10
12
  const clear = () => error.value = void 0;
11
13
  const logout = () => nuxtApp.$auth0.logout();
12
14
  const signup = ({ redirectTo = "/" } = {}) => nuxtApp.$auth0.loginWithRedirect({
@@ -28,6 +30,7 @@ export const useAuth = () => {
28
30
  await nuxtApp.callHook("merkaly:auth:error", { clear, error: reason, logout, retry });
29
31
  };
30
32
  return {
33
+ claims,
31
34
  clear,
32
35
  error,
33
36
  fail,
@@ -0,0 +1,13 @@
1
+ export declare const useOrganization: () => {
2
+ fromServer: import("vue").Ref<boolean, boolean>;
3
+ hasId: import("vue").ComputedRef<boolean>;
4
+ id: import("vue").Ref<string, string>;
5
+ setId: (value: string, options?: {
6
+ server?: boolean;
7
+ }) => void;
8
+ data: import("vue").Ref<import("../internal/state.js").MerkalyOrganization | undefined, import("../internal/state.js").MerkalyOrganization | undefined>;
9
+ error: import("vue").Ref<import("../internal/state.js").BootstrapError | undefined, import("../internal/state.js").BootstrapError | undefined>;
10
+ isReady: import("vue").ComputedRef<boolean>;
11
+ refresh: () => Promise<boolean>;
12
+ status: import("vue").Ref<import("../internal/state.js").BootstrapStatus, import("../internal/state.js").BootstrapStatus>;
13
+ };
@@ -0,0 +1,13 @@
1
+ import { computed, useNuxtApp, useState } from "#imports";
2
+ import { useBootstrapStage } from "../internal/state.js";
3
+ export const useOrganization = () => {
4
+ const id = useState("merkaly:organization:id", () => "");
5
+ const fromServer = useState("merkaly:organization:server", () => false);
6
+ const stage = useBootstrapStage(useNuxtApp(), "organization");
7
+ const hasId = computed(() => Boolean(id.value));
8
+ const setId = (value, options = {}) => {
9
+ id.value = value;
10
+ if (options.server) fromServer.value = true;
11
+ };
12
+ return { ...stage, fromServer, hasId, id, setId };
13
+ };
@@ -0,0 +1,2 @@
1
+ import type { NuxtApp } from '#app';
2
+ export declare const runBootstrap: (nuxtApp: NuxtApp) => Promise<void>;
@@ -0,0 +1,13 @@
1
+ import { useAuth } from "../composables/useAuth.js";
2
+ import { inApp, runBootstrapStage, useBootstrapState } from "./state.js";
3
+ export const runBootstrap = async (nuxtApp) => {
4
+ const { error, fail, isAuthenticated, token } = inApp(nuxtApp, useAuth);
5
+ if (error.value || nuxtApp.payload.error) return;
6
+ const failStage = (stage) => {
7
+ const reason = inApp(nuxtApp, () => useBootstrapState(stage)).error.value;
8
+ return fail({ message: reason?.message || `${stage} failed`, stage, status: reason?.status });
9
+ };
10
+ if (!await runBootstrapStage(nuxtApp, "organization")) return failStage("organization");
11
+ if (!isAuthenticated.value || !token.value) return;
12
+ if (!await runBootstrapStage(nuxtApp, "user")) return failStage("user");
13
+ };
@@ -0,0 +1,45 @@
1
+ import type { NuxtApp } from '#app';
2
+ import type { Ref } from '#imports';
3
+ export interface MerkalyOrganization {
4
+ }
5
+ export interface MerkalyUser {
6
+ }
7
+ export type BootstrapStage = 'organization' | 'user';
8
+ export type BootstrapStatus = 'idle' | 'pending' | 'success' | 'error';
9
+ export interface BootstrapError {
10
+ message: string;
11
+ status?: number;
12
+ }
13
+ interface AdapterLike<T> {
14
+ data: Ref<T>;
15
+ error: Ref<unknown>;
16
+ execute: () => Promise<void>;
17
+ loading: Ref<boolean>;
18
+ }
19
+ export type BootstrapLoader<T = unknown> = () => T | Promise<T> | AdapterLike<T>;
20
+ export interface BootstrapLoaders {
21
+ organization?: BootstrapLoader<MerkalyOrganization>;
22
+ user?: BootstrapLoader<MerkalyUser>;
23
+ }
24
+ interface StageData {
25
+ organization: MerkalyOrganization;
26
+ user: MerkalyUser;
27
+ }
28
+ export declare const inApp: <T>(nuxtApp: NuxtApp, fn: () => T) => T;
29
+ export declare const registerBootstrapLoaders: (nuxtApp: NuxtApp, loaders: BootstrapLoaders) => void;
30
+ export declare const useBootstrapState: <S extends BootstrapStage>(stage: S) => {
31
+ data: Ref<StageData[S] | undefined, StageData[S] | undefined>;
32
+ error: Ref<BootstrapError | undefined, BootstrapError | undefined>;
33
+ status: Ref<BootstrapStatus, BootstrapStatus>;
34
+ };
35
+ export declare const runBootstrapStage: (nuxtApp: NuxtApp, stage: BootstrapStage, { force }?: {
36
+ force?: boolean | undefined;
37
+ }) => Promise<boolean>;
38
+ export declare const useBootstrapStage: <S extends BootstrapStage>(nuxtApp: NuxtApp, stage: S) => {
39
+ data: Ref<StageData[S] | undefined, StageData[S] | undefined>;
40
+ error: Ref<BootstrapError | undefined, BootstrapError | undefined>;
41
+ isReady: import("vue").ComputedRef<boolean>;
42
+ refresh: () => Promise<boolean>;
43
+ status: Ref<BootstrapStatus, BootstrapStatus>;
44
+ };
45
+ export {};
@@ -0,0 +1,51 @@
1
+ import { computed, useState } from "#imports";
2
+ import { until } from "@vueuse/core";
3
+ export const inApp = (nuxtApp, fn) => nuxtApp.runWithContext(fn);
4
+ const registry = /* @__PURE__ */ new WeakMap();
5
+ export const registerBootstrapLoaders = (nuxtApp, loaders) => {
6
+ registry.set(nuxtApp, { ...registry.get(nuxtApp), ...loaders });
7
+ };
8
+ export const useBootstrapState = (stage) => ({
9
+ data: useState(`merkaly:${stage}:data`, () => void 0),
10
+ error: useState(`merkaly:${stage}:error`, () => void 0),
11
+ status: useState(`merkaly:${stage}:status`, () => "idle")
12
+ });
13
+ const isAdapter = (value) => Boolean(value && typeof value === "object" && "execute" in value && "loading" in value);
14
+ const toError = (reason) => {
15
+ const value = reason;
16
+ return {
17
+ message: String(value?.message || reason || "Request failed"),
18
+ status: Number(value?.statusCode ?? value?.status) || void 0
19
+ };
20
+ };
21
+ const resolveLoader = async (nuxtApp, loader) => {
22
+ const result = await nuxtApp.runWithContext(loader);
23
+ if (!isAdapter(result)) return result;
24
+ if (result.loading.value) await until(result.loading).toBe(false);
25
+ else await result.execute();
26
+ if (result.error.value) throw result.error.value;
27
+ return result.data.value;
28
+ };
29
+ export const runBootstrapStage = (nuxtApp, stage, { force = false } = {}) => {
30
+ const loader = registry.get(nuxtApp)?.[stage];
31
+ const { data, error, status } = inApp(nuxtApp, () => useBootstrapState(stage));
32
+ if (!loader) return Promise.resolve(true);
33
+ if (status.value === "success" && !force) return Promise.resolve(true);
34
+ status.value = "pending";
35
+ error.value = void 0;
36
+ return resolveLoader(nuxtApp, loader).then((value) => {
37
+ data.value = value;
38
+ status.value = "success";
39
+ return true;
40
+ }).catch((reason) => {
41
+ error.value = toError(reason);
42
+ status.value = "error";
43
+ return false;
44
+ });
45
+ };
46
+ export const useBootstrapStage = (nuxtApp, stage) => {
47
+ const { data, error, status } = useBootstrapState(stage);
48
+ const isReady = computed(() => status.value === "success");
49
+ const refresh = () => runBootstrapStage(nuxtApp, stage, { force: true });
50
+ return { data, error, isReady, refresh, status };
51
+ };
@@ -1,5 +1,5 @@
1
1
  import { defineNuxtPlugin, useNuxtApp, useRequestURL, useRuntimeConfig } from "#app";
2
- import { useAuth, useTenant } from "#imports";
2
+ import { useAuth, useOrganization } from "#imports";
3
3
  const GLOBAL_API_HEADER = "x-merkaly-global";
4
4
  function serverBaseURL(api, global) {
5
5
  const prefix = global ? "" : (api.prefix ?? "").replace(/^\/+|\/+$/g, "");
@@ -14,7 +14,7 @@ export default defineNuxtPlugin(({ provide }) => {
14
14
  provide("api", async (url, options = {}) => {
15
15
  const { token } = useAuth();
16
16
  if (import.meta.client && token.value) await nuxtApp.$auth0?.getTokenSilently();
17
- const { tenant } = useTenant();
17
+ const { id: tenant } = useOrganization();
18
18
  const { public: { merkaly } } = useRuntimeConfig();
19
19
  const requiresTenant = options.requiresTenant ?? merkaly.auth0.requiresTenant;
20
20
  if (!tenant.value && !options.global && requiresTenant) {
@@ -22,7 +22,7 @@ export default defineNuxtPlugin(({ provide }) => {
22
22
  `[api] Tenant context is required but not set for: ${url}
23
23
  This surface has requiresTenant: true. Possible causes:
24
24
  1. The request fired before the auth boot sequence completed (race condition).
25
- 2. setTenant() was never called in this surface's auth plugin.
25
+ 2. useOrganization().setId() was never called in this surface's auth plugin.
26
26
  3. The adapter should use { global: true } to bypass tenant enforcement.`
27
27
  );
28
28
  }
@@ -3,8 +3,10 @@ import { defineNuxtPlugin, useRuntimeConfig, useNuxtApp, useState } from "#impor
3
3
  import { navigateTo } from "#app";
4
4
  import { defu } from "defu";
5
5
  import { useAuth } from "../composables/useAuth.js";
6
+ import { runBootstrap } from "../internal/bootstrap.js";
6
7
  const NO_SESSION_ERRORS = ["consent_required", "interaction_required", "invalid_grant", "login_required", "missing_refresh_token"];
7
- export default defineNuxtPlugin(async ({ callHook, hook }) => {
8
+ export default defineNuxtPlugin(async (nuxtApp) => {
9
+ const { callHook, hook } = nuxtApp;
8
10
  const { public: $config } = useRuntimeConfig();
9
11
  const callbackPath = URL.canParse($config.merkaly.auth0.callbackUrl) ? new URL($config.merkaly.auth0.callbackUrl).pathname : $config.merkaly.auth0.callbackUrl;
10
12
  const auth0 = await createAuth0Client({
@@ -13,11 +15,11 @@ export default defineNuxtPlugin(async ({ callHook, hook }) => {
13
15
  domain: $config.merkaly.auth0.domain,
14
16
  useRefreshTokens: true
15
17
  });
16
- const { error, isLoading, user, token, fail } = useAuth();
18
+ const { claims, error, isLoading, token, fail } = useAuth();
17
19
  const isStarting = useState("auth:starting", () => false);
18
20
  const self0 = Object.assign(Object.create(Object.getPrototypeOf(auth0)), auth0);
19
21
  let tokenError;
20
- auth0.getUser = () => self0.getUser().then((result) => user.value = result).catch((reason) => console.error("[Auth0] getUser failed", reason));
22
+ auth0.getUser = () => self0.getUser().then((result) => claims.value = result).catch((reason) => console.error("[Auth0] getUser failed", reason));
21
23
  auth0.getTokenSilently = (options = {}) => self0.getTokenSilently(defu({ authorizationParams: { audience: $config.merkaly.auth0.audience } }, options)).then((result) => {
22
24
  tokenError = void 0;
23
25
  return token.value = result;
@@ -28,26 +30,27 @@ export default defineNuxtPlugin(async ({ callHook, hook }) => {
28
30
  const resolveIdentity = () => Promise.allSettled([auth0.getUser(), auth0.getTokenSilently()]);
29
31
  const initTenantContext = async () => {
30
32
  try {
31
- await callHook("merkaly:tenant", user.value);
32
- await callHook("merkaly:auth", user.value);
33
+ await callHook("merkaly:tenant", claims.value);
34
+ await callHook("merkaly:auth", claims.value);
33
35
  } catch (reason) {
34
36
  if (!error.value) {
35
37
  await fail({ message: reason?.message || String(reason), stage: "tenant" });
36
38
  }
37
39
  }
40
+ await runBootstrap(nuxtApp);
38
41
  };
39
42
  const startSession = async () => {
40
- if (user.value && !token.value && tokenError && !NO_SESSION_ERRORS.includes(tokenError.error)) {
43
+ if (claims.value && !token.value && tokenError && !NO_SESSION_ERRORS.includes(tokenError.error)) {
41
44
  return fail({ message: tokenError.error_description || tokenError.message, stage: "token" });
42
45
  }
43
- if (user.value && !token.value && tokenError) {
44
- user.value = void 0;
46
+ if (claims.value && !token.value && tokenError) {
47
+ claims.value = void 0;
45
48
  await self0.logout({ openUrl: false });
46
49
  if ($config.merkaly.auth0.requiresAuth) {
47
50
  return auth0.loginWithRedirect({ appState: { target: location.pathname + location.search } });
48
51
  }
49
52
  }
50
- if (!user.value && $config.merkaly.auth0.requiresAuth) {
53
+ if (!claims.value && $config.merkaly.auth0.requiresAuth) {
51
54
  return auth0.loginWithRedirect({ appState: { target: location.pathname + location.search } });
52
55
  }
53
56
  return initTenantContext();
@@ -86,14 +89,14 @@ export default defineNuxtPlugin(async ({ callHook, hook }) => {
86
89
  auth0.linkWithConnection = async (connection) => {
87
90
  const client = getLinkingClient();
88
91
  await client.loginWithPopup({ authorizationParams: { connection } });
89
- const claims = await client.getIdTokenClaims();
90
- if (!claims?.__raw) {
92
+ const idClaims = await client.getIdTokenClaims();
93
+ if (!idClaims?.__raw) {
91
94
  throw new Error("Failed to get ID token for linking");
92
95
  }
93
96
  const { $api } = useNuxtApp();
94
97
  return await $api("/auth/identities", {
95
98
  // the api verifies it and derives the identity
96
- body: { idToken: claims.__raw },
99
+ body: { idToken: idClaims.__raw },
97
100
  global: true,
98
101
  method: "POST"
99
102
  });
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from '#app';
2
+ declare const plugin: Plugin;
3
+ export default plugin;
@@ -0,0 +1,6 @@
1
+ import { defineNuxtPlugin } from "#imports";
2
+ import { runBootstrap } from "../internal/bootstrap.js";
3
+ const plugin = defineNuxtPlugin((nuxtApp) => {
4
+ nuxtApp.hook("app:created", () => runBootstrap(nuxtApp));
5
+ });
6
+ export default plugin;
@@ -14,9 +14,9 @@ export default defineNuxtPlugin(() => {
14
14
  // first-party proxy
15
15
  ui_host: "https://us.posthog.com"
16
16
  });
17
- const { user } = useAuth();
18
- watchImmediate(() => user.value?.sub, (sub, previous) => {
19
- if (sub) return posthog.identify(sub, { email: user.value?.email, name: user.value?.name });
17
+ const { claims } = useAuth();
18
+ watchImmediate(() => claims.value?.sub, (sub, previous) => {
19
+ if (sub) return posthog.identify(sub, { email: claims.value?.email, name: claims.value?.name });
20
20
  if (previous) posthog.reset();
21
21
  });
22
22
  });
@@ -15,10 +15,10 @@ declare module '#app' {
15
15
  }
16
16
 
17
17
  interface RuntimeNuxtHooks {
18
- 'merkaly:auth': (user?: User) => HookResult;
18
+ 'merkaly:auth': (claims?: User) => HookResult;
19
19
  'merkaly:auth:error': (context: AuthErrorContext) => HookResult;
20
20
  'merkaly:auth:retry': () => HookResult;
21
- 'merkaly:tenant': (user?: User) => HookResult;
21
+ 'merkaly:tenant': (claims?: User) => HookResult;
22
22
  }
23
23
  }
24
24
 
@@ -0,0 +1,2 @@
1
+ import type { BootstrapLoaders } from '../internal/state.js';
2
+ export declare const defineBootstrap: (loaders: BootstrapLoaders) => void;
@@ -0,0 +1,3 @@
1
+ import { useNuxtApp } from "#imports";
2
+ import { registerBootstrapLoaders } from "../internal/state.js";
3
+ export const defineBootstrap = (loaders) => registerBootstrapLoaders(useNuxtApp(), loaders);
package/dist/types.d.mts CHANGED
@@ -8,6 +8,8 @@ export { type AdapterArgs, type AdapterOptions } from '../dist/runtime/utils/wit
8
8
 
9
9
  export { type ApiOptions, type HooksOptions, type ParamsOptions, type RefOptions } from '../dist/runtime/plugins/api.global.js'
10
10
 
11
+ export { type BootstrapError, type BootstrapLoader, type BootstrapLoaders, type BootstrapStatus, type MerkalyOrganization, type MerkalyUser } from '../dist/runtime/internal/state.js'
12
+
11
13
  export { default } from './module.mjs'
12
14
 
13
15
  export { type MerkalyI18nLocale, type MerkalyModuleOptions } from './module.mjs'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merkaly/nuxt",
3
- "version": "0.12.5",
3
+ "version": "0.13.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/merkaly-io/nuxt.git"
@@ -1,8 +0,0 @@
1
- export declare const useTenant: () => {
2
- fromServer: import("vue").Ref<boolean, boolean>;
3
- isReady: import("vue").ComputedRef<boolean>;
4
- setTenant: (value: string, options?: {
5
- server?: boolean;
6
- }) => void;
7
- tenant: import("vue").Ref<string, string>;
8
- };
@@ -1,17 +0,0 @@
1
- import { computed } from "vue";
2
- import { useState } from "#imports";
3
- export const useTenant = () => {
4
- const tenant = useState("tenant:id", () => "");
5
- const fromServer = useState("tenant:server", () => false);
6
- const isReady = computed(() => Boolean(tenant.value));
7
- const setTenant = (value, options = {}) => {
8
- tenant.value = value;
9
- if (options.server) fromServer.value = true;
10
- };
11
- return {
12
- fromServer,
13
- isReady,
14
- setTenant,
15
- tenant
16
- };
17
- };