@strivacity/sdk-nuxt 4.0.0-beta.1 → 4.0.0-beta.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 (124) hide show
  1. package/README.md +171 -101
  2. package/dist/module.mjs +5 -4
  3. package/package.json +2 -2
  4. package/project.json +3 -0
  5. package/src/module.ts +7 -3
  6. package/src/runtime/composables/use-native-login.ts +27 -159
  7. package/src/runtime/composables/use-strivacity.ts +217 -67
  8. package/src/runtime/middleware/auth.server.ts +10 -1
  9. package/src/runtime/plugins/strivacity.client.ts +17 -0
  10. package/src/runtime/server/plugins/auth.server.ts +66 -30
  11. package/src/runtime/storages/default.ts +22 -67
  12. package/src/runtime/types.ts +7 -36
  13. package/src/runtime/utils/core.ts +2 -0
  14. package/testing/stubs/app.ts +8 -0
  15. package/testing/tests/composables.spec.ts +104 -3
  16. package/testing/tests/module.spec.ts +46 -0
  17. package/testing/tests/server/plugin.spec.ts +48 -10
  18. package/testing/tests/server/routes.spec.ts +2 -2
  19. package/.nuxt/app-component.mjs +0 -1
  20. package/.nuxt/app.config.mjs +0 -21
  21. package/.nuxt/component-chunk.mjs +0 -1
  22. package/.nuxt/component-names.mjs +0 -1
  23. package/.nuxt/components.d.ts +0 -66
  24. package/.nuxt/components.islands.mjs +0 -8
  25. package/.nuxt/components.plugin.mjs +0 -5
  26. package/.nuxt/css.mjs +0 -0
  27. package/.nuxt/error-component.mjs +0 -1
  28. package/.nuxt/fetch.d.ts +0 -1
  29. package/.nuxt/fetch.mjs +0 -8
  30. package/.nuxt/global-polyfills.mjs +0 -4
  31. package/.nuxt/imports.d.ts +0 -41
  32. package/.nuxt/imports.mjs +0 -42
  33. package/.nuxt/island-renderer.mjs +0 -2
  34. package/.nuxt/layouts.mjs +0 -2
  35. package/.nuxt/manifest/meta/b2d6b125-90fd-44d4-9916-0898f71676da.json +0 -1
  36. package/.nuxt/middleware.mjs +0 -7
  37. package/.nuxt/nitro.client.mjs +0 -2
  38. package/.nuxt/nuxt.config.mjs +0 -103
  39. package/.nuxt/nuxt.d.ts +0 -19
  40. package/.nuxt/nuxt.node.d.ts +0 -13
  41. package/.nuxt/nuxt.shared.d.ts +0 -7
  42. package/.nuxt/pages.mjs +0 -2
  43. package/.nuxt/paths.mjs +0 -15
  44. package/.nuxt/plugins.client.mjs +0 -14
  45. package/.nuxt/plugins.server.mjs +0 -10
  46. package/.nuxt/root-component.mjs +0 -1
  47. package/.nuxt/route-rules.mjs +0 -22
  48. package/.nuxt/router.options.mjs +0 -2
  49. package/.nuxt/schema/nuxt.schema.d.ts +0 -17
  50. package/.nuxt/schema/nuxt.schema.json +0 -3
  51. package/.nuxt/test-component-wrapper.mjs +0 -1
  52. package/.nuxt/tsconfig.app.json +0 -211
  53. package/.nuxt/tsconfig.json +0 -212
  54. package/.nuxt/tsconfig.node.json +0 -117
  55. package/.nuxt/tsconfig.server.json +0 -161
  56. package/.nuxt/tsconfig.shared.json +0 -165
  57. package/.nuxt/types/app.config.d.ts +0 -35
  58. package/.nuxt/types/build.d.ts +0 -0
  59. package/.nuxt/types/builder-env.d.ts +0 -1
  60. package/.nuxt/types/components.d.ts +0 -71
  61. package/.nuxt/types/imports.d.ts +0 -2
  62. package/.nuxt/types/layouts.d.ts +0 -16
  63. package/.nuxt/types/middleware.d.ts +0 -11
  64. package/.nuxt/types/modules.d.ts +0 -59
  65. package/.nuxt/types/nitro-config.d.ts +0 -14
  66. package/.nuxt/types/nitro-imports.d.ts +0 -20
  67. package/.nuxt/types/nitro-layouts.d.ts +0 -17
  68. package/.nuxt/types/nitro-nuxt.d.ts +0 -75
  69. package/.nuxt/types/nitro-routes.d.ts +0 -38
  70. package/.nuxt/types/nitro.d.ts +0 -3
  71. package/.nuxt/types/plugins.d.ts +0 -30
  72. package/.nuxt/types/runtime-config.d.ts +0 -38
  73. package/.nuxt/types/shared-app.config.d.ts +0 -28
  74. package/.nuxt/types/shared-imports.d.ts +0 -2
  75. package/.nuxt/types/vue-shim.d.ts +0 -0
  76. package/.nuxt/unhead-options.mjs +0 -5
  77. package/.nuxt/unhead.config.mjs +0 -1
  78. package/dist/module.d.mts +0 -7
  79. package/dist/module.json +0 -9
  80. package/dist/module.mjs.map +0 -1
  81. package/dist/runtime/composables/use-native-login-context.d.ts +0 -10
  82. package/dist/runtime/composables/use-native-login-context.js +0 -9
  83. package/dist/runtime/composables/use-native-login.d.ts +0 -10
  84. package/dist/runtime/composables/use-native-login.js +0 -143
  85. package/dist/runtime/composables/use-session.d.ts +0 -8
  86. package/dist/runtime/composables/use-session.js +0 -2
  87. package/dist/runtime/composables/use-strivacity.d.ts +0 -9
  88. package/dist/runtime/composables/use-strivacity.js +0 -77
  89. package/dist/runtime/middleware/auth.server.d.ts +0 -2
  90. package/dist/runtime/middleware/auth.server.js +0 -12
  91. package/dist/runtime/server/api/auth/backchannel-logout.get.d.ts +0 -2
  92. package/dist/runtime/server/api/auth/backchannel-logout.get.js +0 -2
  93. package/dist/runtime/server/api/auth/callback.get.d.ts +0 -2
  94. package/dist/runtime/server/api/auth/callback.get.js +0 -2
  95. package/dist/runtime/server/api/auth/entry.get.d.ts +0 -2
  96. package/dist/runtime/server/api/auth/entry.get.js +0 -2
  97. package/dist/runtime/server/api/auth/login.get.d.ts +0 -2
  98. package/dist/runtime/server/api/auth/login.get.js +0 -2
  99. package/dist/runtime/server/api/auth/logout.get.d.ts +0 -2
  100. package/dist/runtime/server/api/auth/logout.get.js +0 -2
  101. package/dist/runtime/server/api/auth/refresh.get.d.ts +0 -2
  102. package/dist/runtime/server/api/auth/refresh.get.js +0 -2
  103. package/dist/runtime/server/api/auth/register.get.d.ts +0 -2
  104. package/dist/runtime/server/api/auth/register.get.js +0 -2
  105. package/dist/runtime/server/api/auth/revoke.get.d.ts +0 -2
  106. package/dist/runtime/server/api/auth/revoke.get.js +0 -2
  107. package/dist/runtime/server/composables/use-strivacity.d.ts +0 -2
  108. package/dist/runtime/server/composables/use-strivacity.js +0 -9
  109. package/dist/runtime/server/plugins/auth.server.d.ts +0 -10
  110. package/dist/runtime/server/plugins/auth.server.js +0 -66
  111. package/dist/runtime/server/utils/helpers.d.ts +0 -18
  112. package/dist/runtime/server/utils/helpers.js +0 -39
  113. package/dist/runtime/storages/core.d.ts +0 -1
  114. package/dist/runtime/storages/core.js +0 -13
  115. package/dist/runtime/storages/default.d.ts +0 -21
  116. package/dist/runtime/storages/default.js +0 -71
  117. package/dist/runtime/types.d.ts +0 -219
  118. package/dist/runtime/types.js +0 -1
  119. package/dist/runtime/utils/core.d.ts +0 -1
  120. package/dist/runtime/utils/core.js +0 -64
  121. package/dist/runtime/utils/noop.d.ts +0 -1
  122. package/dist/runtime/utils/noop.js +0 -3
  123. package/dist/types.d.mts +0 -9
  124. /package/src/runtime/server/api/auth/{backchannel-logout.get.ts → backchannel-logout.post.ts} +0 -0
@@ -1,9 +1,8 @@
1
- import type { NativeFlow, NativeParams, NativeFlowState, NativeFlowMessage } from '@strivacity/sdk-core/types';
1
+ import type { NativeFlow, NativeFlowState, NativeFlowMessage } from '@strivacity/sdk-core/types';
2
2
  import type { LoginContext, UseNativeLoginOptions } from '../types';
3
3
  import { STRIVACITY_LOGIN_CONTEXT, type useNativeLoginContext } from './use-native-login-context';
4
4
  import { provide, ref, onMounted, onUnmounted } from 'vue';
5
- import { FallbackError } from '@strivacity/sdk-core/utils/errors';
6
- import { isSessionExpired, unflattenObject } from '@strivacity/sdk-core/utils';
5
+ import { createNativeLoginController } from '@strivacity/sdk-core/utils';
7
6
  import { useStrivacity } from './use-strivacity';
8
7
 
9
8
  /**
@@ -14,169 +13,38 @@ import { useStrivacity } from './use-strivacity';
14
13
  * @throws {Error} - If the Strivacity SDK context is not found.
15
14
  */
16
15
  export function useNativeLogin(options: UseNativeLoginOptions = {}): ReturnType<typeof useNativeLoginContext> {
17
- const abortController = new AbortController();
18
16
  const { sdk } = useStrivacity<NativeFlow>();
19
-
20
- const loading = ref<boolean>(true);
21
- const forms = ref<Record<string, Record<string, unknown>>>({});
22
- const messages = ref<Record<string, Record<string, NativeFlowMessage>>>({});
23
- const state = ref<Partial<NativeFlowState>>({});
24
-
25
- onMounted(async () => {
26
- await sdk.init();
27
-
28
- try {
29
- await startSession(options.params);
30
- } catch (error) {
31
- if (error instanceof DOMException && error.name === 'AbortError') {
32
- return;
33
- }
34
-
35
- sdk.logging?.error('Error fetching authorization URI', error as Error);
36
- void options.onError?.(error as Error);
37
- loading.value = false;
38
- }
17
+ const controller = createNativeLoginController(() => sdk, options);
18
+ const snapshot = controller.getSnapshot();
19
+
20
+ const loading = ref<boolean>(snapshot.loading);
21
+ const forms = ref<Record<string, Record<string, unknown>>>(snapshot.forms);
22
+ const messages = ref<Record<string, Record<string, NativeFlowMessage>>>(snapshot.messages);
23
+ const state = ref<Partial<NativeFlowState>>(snapshot.state);
24
+
25
+ const unsubscribe = controller.subscribe((next) => {
26
+ loading.value = next.loading;
27
+ forms.value = next.forms;
28
+ messages.value = next.messages;
29
+ state.value = next.state;
39
30
  });
40
31
 
41
- onUnmounted(() => abortController.abort());
42
-
43
- function handleResponse(nextState: Partial<NativeFlowState>) {
44
- if (sdk.session && !isSessionExpired(sdk.session)) {
45
- return void options.onLogin?.(sdk.session);
46
- }
47
-
48
- const newState: NativeFlowState = {
49
- hostedUrl: nextState?.hostedUrl ?? state.value.hostedUrl,
50
- finalizeUrl: nextState?.finalizeUrl ?? state.value.finalizeUrl,
51
- screen: nextState?.screen ?? state.value.screen,
52
- forms: nextState?.forms ?? state.value.forms,
53
- layout: nextState?.layout ?? state.value.layout,
54
- messages: nextState?.messages ?? {},
55
- branding: nextState?.branding ?? state.value.branding,
56
- };
57
-
58
- if (newState.screen !== state.value.screen) {
59
- forms.value = {};
60
- messages.value = {};
61
-
62
- for (const form of newState.forms ?? []) {
63
- forms.value[form.id] = {};
64
- messages.value[form.id] = {};
65
- }
66
- } else {
67
- sdk.logging?.info(`Updating screen: ${newState.screen}`);
68
- }
69
-
70
- for (const formId of Object.keys(newState.messages ?? {})) {
71
- if (formId === 'global') {
72
- // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
73
- void options.onGlobalMessage?.(newState.messages?.global!);
74
- } else {
75
- messages.value[formId] = newState.messages![formId] ?? {};
76
- }
77
- }
78
-
79
- state.value = newState;
80
-
81
- if (!newState.finalizeUrl) {
82
- loading.value = false;
83
- }
84
- }
85
-
86
- async function startSession(loginParams: NativeParams = {}) {
87
- try {
88
- loading.value = true;
89
-
90
- const nextState = await sdk.startSession(loginParams);
91
-
92
- if (nextState) {
93
- handleResponse(nextState);
94
- }
95
- } catch (error) {
96
- if (error instanceof FallbackError) {
97
- sdk.logging?.error('Fallback error occurred', error);
98
- void options.onFallback?.(error);
99
- } else {
100
- sdk.logging?.error('Error starting session', error as Error);
101
- void options.onError?.(error as Error);
102
- }
103
- }
104
- }
105
-
106
- function triggerFallback(message?: string) {
107
- sdk.logging?.warn(message ? `Triggering fallback due to: ${message}` : 'Triggering fallback');
108
-
109
- if (!state.value.hostedUrl) {
110
- const error = new Error('No hosted URL provided');
111
- sdk.logging?.error('Fallback error', error);
112
- throw error;
113
- }
114
-
115
- void options.onFallback?.(new FallbackError(new URL(state.value.hostedUrl)));
116
- }
32
+ onMounted(() => controller.start());
33
+ onUnmounted(unsubscribe);
117
34
 
118
- function triggerClose() {
119
- sdk.logging?.debug('Triggering close');
120
- void options.onClose?.();
121
- }
122
-
123
- async function submitForm(formId: string, customBody?: Record<string, unknown>) {
124
- try {
125
- loading.value = true;
126
-
127
- const nextState = await sdk.submitForm(formId, customBody ?? unflattenObject(forms.value[formId] ?? {}));
128
-
129
- if (nextState) {
130
- handleResponse(nextState);
131
- }
132
- } catch (error) {
133
- if (error instanceof FallbackError) {
134
- sdk.logging?.error('Fallback error occurred', error);
135
- void options.onFallback?.(error);
136
- } else {
137
- sdk.logging?.error('Error submitting form', error as Error);
138
- void options.onError?.(error as Error);
139
- }
140
- }
141
- }
142
-
143
- function setFormValue(formId: string, widgetId: string, value: unknown) {
144
- if (forms.value[formId] === undefined) {
145
- forms.value[formId] = {};
146
- }
147
-
148
- forms.value[formId][widgetId] = value === '' ? null : value;
149
- }
150
-
151
- function setMessage(formId: string, widgetId: string, value: NativeFlowMessage) {
152
- if (messages.value[formId] === undefined) {
153
- messages.value[formId] = {};
154
- }
155
-
156
- messages.value[formId][widgetId] = value;
157
- }
158
-
159
- provide<LoginContext>(STRIVACITY_LOGIN_CONTEXT, {
35
+ const context: LoginContext = {
160
36
  loading,
161
37
  forms,
162
38
  messages,
163
39
  state,
164
- triggerFallback,
165
- triggerClose,
166
- submitForm,
167
- setFormValue,
168
- setMessage,
169
- });
170
-
171
- return {
172
- loading,
173
- forms,
174
- messages,
175
- state,
176
- triggerFallback,
177
- triggerClose,
178
- submitForm,
179
- setFormValue,
180
- setMessage,
40
+ triggerFallback: controller.triggerFallback,
41
+ triggerClose: controller.triggerClose,
42
+ submitForm: controller.submitForm,
43
+ setFormValue: controller.setFormValue,
44
+ setMessage: controller.setMessage,
181
45
  };
46
+
47
+ provide<LoginContext>(STRIVACITY_LOGIN_CONTEXT, context);
48
+
49
+ return context;
182
50
  }
@@ -1,95 +1,245 @@
1
- import type { EmbeddedFlow, IdTokenClaims, NativeFlow, PopupFlow, RedirectFlow } from '@strivacity/sdk-core/types';
1
+ import type { EmbeddedFlow, IdTokenClaims, NativeFlow, PopupFlow, RedirectFlow, SessionData } from '@strivacity/sdk-core/types';
2
2
  import type { SDKContext } from '../types';
3
- import { ref } from 'vue';
3
+ import { computed, ref } from 'vue';
4
4
  import { initFlow } from '@strivacity/sdk-core';
5
- import { useRuntimeConfig } from '#app';
5
+ import { isSessionExpired } from '@strivacity/sdk-core/utils';
6
+ import { tryUseNuxtApp, useRuntimeConfig } from '#app';
6
7
  import { useSession } from './use-session';
7
8
  import createLogging from '#strivacity-options-logging';
8
9
  import createHttpClient from '#strivacity-options-httpClient';
9
10
 
10
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
- let sdk: any;
11
+ type Flow = RedirectFlow | PopupFlow | NativeFlow | EmbeddedFlow;
12
12
 
13
- const loadingRef = ref<boolean>(true);
14
- const languageRef = ref<string>(globalThis.navigator?.language ?? 'en-US');
15
- const isAuthenticatedRef = ref<boolean>(false);
16
- const idTokenClaimsRef = ref<IdTokenClaims | null>(null);
17
- const accessTokenRef = ref<string | null>(null);
18
- const refreshTokenRef = ref<string | null>(null);
19
- const accessTokenExpiredRef = ref<boolean>(true);
20
- const accessTokenExpirationDateRef = ref<number | null>(null);
13
+ // Browser-only singleton: there is exactly one user per page there. It is never created during SSR, where a module-level
14
+ // instance would be shared by every concurrent request (and leak one user's session into another user's HTML).
15
+ let clientContext: SDKContext<Flow> | undefined;
21
16
 
22
- const updateSession = async () => {
23
- const authenticated = await sdk.isAuthenticated;
24
-
25
- if (loadingRef.value) {
26
- loadingRef.value = false;
27
- }
28
- if (sdk.language !== languageRef.value) {
29
- languageRef.value = sdk.language;
30
- }
31
- if (authenticated !== isAuthenticatedRef.value) {
32
- isAuthenticatedRef.value = authenticated;
33
- }
34
- if (sdk.idTokenClaims !== idTokenClaimsRef.value) {
35
- idTokenClaimsRef.value = sdk.idTokenClaims || null;
36
- }
37
- if (sdk.accessToken !== accessTokenRef.value) {
38
- accessTokenRef.value = sdk.accessToken || null;
39
- }
40
- if (sdk.refreshToken !== refreshTokenRef.value) {
41
- refreshTokenRef.value = sdk.refreshToken || null;
42
- }
43
- if (sdk.accessTokenExpired !== accessTokenExpiredRef.value) {
44
- accessTokenExpiredRef.value = sdk.accessTokenExpired;
45
- }
46
- if (sdk.accessTokenExpirationDate !== accessTokenExpirationDateRef.value) {
47
- accessTokenExpirationDateRef.value = sdk.accessTokenExpirationDate || null;
48
- }
49
- };
17
+ /**
18
+ * Derives the reactive state from a session. Used by the SSR context, and as the browser's initial state, so the first client
19
+ * render matches the server-rendered HTML (no hydration mismatch) until the browser SDK takes over.
20
+ *
21
+ * @param {SessionData | undefined} session - The current session (`useSession()`).
22
+ * @param {boolean} serverManaged - Whether server-managed sessions are enabled (`serverSessionUri`).
23
+ * @returns The derived state values.
24
+ */
25
+ function deriveState(session: SessionData | undefined, serverManaged: boolean) {
26
+ return {
27
+ // With client-managed sessions the server cannot know the session (it lives in browser storage), so stay "loading"
28
+ // until the browser SDK has loaded it.
29
+ loading: !serverManaged,
30
+ language: 'en-US',
31
+ isAuthenticated: serverManaged && !isSessionExpired(session),
32
+ idTokenClaims: session?.claims ?? null,
33
+ accessToken: session?.access_token ?? null,
34
+ accessTokenExpired: isSessionExpired(session),
35
+ accessTokenExpirationDate: session?.expires_at ?? null,
36
+ };
37
+ }
50
38
 
51
39
  /**
52
- * A composable to access the Strivacity SDK context.
40
+ * Creates the browser SDK instance and its reactive state. Called once, by the `strivacity` client plugin (or lazily by
41
+ * the first `useStrivacity()` call in the browser).
53
42
  *
54
- * @template T - The type of the Strivacity SDK flow (RedirectFlow, PopupFlow, NativeFlow, or EmbeddedFlow).
55
- * @returns {SDKContext<T>} The Strivacity SDK context.
43
+ * @returns {SDKContext} The browser SDK context.
56
44
  */
57
- export const useStrivacity = <
58
- T extends RedirectFlow | PopupFlow | NativeFlow | EmbeddedFlow = RedirectFlow | PopupFlow | NativeFlow | EmbeddedFlow,
59
- >(): SDKContext<T> => {
60
- if (!sdk) {
61
- sdk = initFlow({
62
- ...useRuntimeConfig().public.strivacity,
63
- logging: createLogging ? createLogging() : undefined,
64
- httpClient: createHttpClient ? createHttpClient() : undefined,
65
- });
66
- sdk.subscribeToAllEvents(updateSession);
67
-
68
- sdk.session = useSession().value;
69
- void updateSession();
45
+ export function getClientContext(): SDKContext<Flow> {
46
+ if (clientContext) {
47
+ return clientContext;
70
48
  }
71
49
 
72
- return {
50
+ const options = useRuntimeConfig().public.strivacity;
51
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
+ const sdk: any = initFlow({
53
+ ...options,
54
+ logging: createLogging ? createLogging() : undefined,
55
+ httpClient: createHttpClient ? createHttpClient() : undefined,
56
+ });
57
+
58
+ // Start from the same values the server rendered with, then let the SDK update them.
59
+ const initial = deriveState(useSession().value, !!options.serverSessionUri);
60
+ const loading = ref<boolean>(initial.loading);
61
+ const language = ref<string>(initial.language);
62
+ const isAuthenticated = ref<boolean>(initial.isAuthenticated);
63
+ const idTokenClaims = ref<IdTokenClaims | null>(initial.idTokenClaims);
64
+ const accessToken = ref<string | null>(initial.accessToken);
65
+ const refreshToken = ref<string | null>(null);
66
+ const accessTokenExpired = ref<boolean>(initial.accessTokenExpired);
67
+ const accessTokenExpirationDate = ref<number | null>(initial.accessTokenExpirationDate);
68
+
69
+ const updateSession = async () => {
70
+ const authenticated = await sdk.isAuthenticated;
71
+
72
+ if (loading.value) {
73
+ loading.value = false;
74
+ }
75
+ if (sdk.language !== language.value) {
76
+ language.value = sdk.language;
77
+ }
78
+ if (authenticated !== isAuthenticated.value) {
79
+ isAuthenticated.value = authenticated;
80
+ }
81
+ if (sdk.idTokenClaims !== idTokenClaims.value) {
82
+ idTokenClaims.value = sdk.idTokenClaims || null;
83
+ }
84
+ if (sdk.accessToken !== accessToken.value) {
85
+ accessToken.value = sdk.accessToken || null;
86
+ }
87
+ if (sdk.refreshToken !== refreshToken.value) {
88
+ refreshToken.value = sdk.refreshToken || null;
89
+ }
90
+ if (sdk.accessTokenExpired !== accessTokenExpired.value) {
91
+ accessTokenExpired.value = sdk.accessTokenExpired;
92
+ }
93
+ if (sdk.accessTokenExpirationDate !== accessTokenExpirationDate.value) {
94
+ accessTokenExpirationDate.value = sdk.accessTokenExpirationDate || null;
95
+ }
96
+ };
97
+
98
+ sdk.subscribeToAllEvents(updateSession);
99
+ sdk.session = useSession().value;
100
+ void updateSession();
101
+
102
+ clientContext = {
73
103
  sdk,
74
- loading: loadingRef,
75
- language: languageRef,
76
- isAuthenticated: isAuthenticatedRef,
77
- idTokenClaims: idTokenClaimsRef,
78
- accessToken: accessTokenRef,
79
- refreshToken: refreshTokenRef,
80
- accessTokenExpired: accessTokenExpiredRef,
81
- accessTokenExpirationDate: accessTokenExpirationDateRef,
104
+ loading,
105
+ language,
106
+ isAuthenticated,
107
+ idTokenClaims,
108
+ accessToken,
109
+ refreshToken,
110
+ accessTokenExpired,
111
+ accessTokenExpirationDate,
82
112
  init: () => sdk.init(),
83
113
  subscribeToEvent: ((...args: Parameters<typeof sdk.subscribeToEvent>) => sdk.subscribeToEvent(...args)) as typeof sdk.subscribeToEvent,
84
114
  subscribeToAllEvents: (...args: Parameters<typeof sdk.subscribeToAllEvents>) => sdk.subscribeToAllEvents(...args),
85
115
  checkAuthentication: (...args: Parameters<typeof sdk.checkAuthentication>) => sdk.checkAuthentication(...args),
86
116
  tokenExchange: (...args: Parameters<typeof sdk.tokenExchange>) => sdk.tokenExchange(...args),
87
117
  handleCallback: (...args: Parameters<typeof sdk.handleCallback>) => sdk.handleCallback(...args),
88
- refresh: () => sdk.refresh(),
118
+ refresh: (...args: Parameters<typeof sdk.refresh>) => sdk.refresh(...args),
89
119
  revoke: () => sdk.revoke(),
90
120
  logout: (...args: Parameters<typeof sdk.logout>) => sdk.logout(...args),
91
121
  login: (...args: Parameters<typeof sdk.login>) => sdk.login(...args),
92
122
  register: (...args: Parameters<typeof sdk.register>) => sdk.register(...args),
93
123
  entry: (...args: Parameters<typeof sdk.entry>) => sdk.entry(...args),
94
124
  };
125
+
126
+ return clientContext;
127
+ }
128
+
129
+ const SDK_METHODS = new Set([
130
+ 'init',
131
+ 'login',
132
+ 'register',
133
+ 'entry',
134
+ 'handleCallback',
135
+ 'tokenExchange',
136
+ 'refresh',
137
+ 'revoke',
138
+ 'logout',
139
+ 'updateSession',
140
+ 'cleanupSession',
141
+ 'startSession',
142
+ 'submitForm',
143
+ 'finalizeSession',
144
+ ]);
145
+
146
+ function browserOnly(name: string): () => never {
147
+ return () => {
148
+ throw new Error(`[@strivacity/sdk-nuxt] ${name}() is only available in the browser. During SSR, useStrivacity() is read-only.`);
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Builds a read-only context for server-side rendering from the current request's session (`useSession()`, filled by the
154
+ * module's server middleware when server-managed sessions are enabled). No SDK instance is created on the server.
155
+ *
156
+ * @returns {SDKContext} The read-only SSR context.
157
+ */
158
+ function createServerContext(): SDKContext<Flow> {
159
+ const options = useRuntimeConfig().public.strivacity;
160
+ const session = useSession();
161
+ const serverManaged = !!options.serverSessionUri;
162
+
163
+ const state = computed(() => deriveState(session.value, serverManaged));
164
+ const isAuthenticated = computed(() => state.value.isAuthenticated);
165
+ const idTokenClaims = computed(() => state.value.idTokenClaims);
166
+ const accessToken = computed(() => state.value.accessToken);
167
+ const accessTokenExpired = computed(() => state.value.accessTokenExpired);
168
+ const accessTokenExpirationDate = computed(() => state.value.accessTokenExpirationDate);
169
+ const noopSubscription = () => ({ dispose: () => undefined });
170
+
171
+ const values: Record<string, () => unknown> = {
172
+ options: () => options,
173
+ session: () => session.value ?? null,
174
+ isAuthenticated: async () => Promise.resolve(isAuthenticated.value),
175
+ isAuthenticatedSync: () => isAuthenticated.value,
176
+ idTokenClaims: () => idTokenClaims.value,
177
+ accessToken: () => accessToken.value,
178
+ accessTokenExpired: () => accessTokenExpired.value,
179
+ accessTokenExpirationDate: () => accessTokenExpirationDate.value,
180
+ subscribeToEvent: () => noopSubscription,
181
+ subscribeToAllEvents: () => noopSubscription,
182
+ checkAuthentication: () => async () => Promise.resolve(isAuthenticated.value),
183
+ };
184
+
185
+ const sdk = new Proxy(
186
+ {},
187
+ {
188
+ get(_target, property) {
189
+ if (typeof property !== 'string') {
190
+ return undefined;
191
+ }
192
+ if (property in values) {
193
+ return values[property]!();
194
+ }
195
+ if (SDK_METHODS.has(property)) {
196
+ return browserOnly(`sdk.${property}`);
197
+ }
198
+
199
+ return undefined;
200
+ },
201
+ },
202
+ ) as Flow;
203
+
204
+ return {
205
+ sdk,
206
+ loading: ref<boolean>(state.value.loading),
207
+ language: ref<string>(state.value.language),
208
+ isAuthenticated,
209
+ idTokenClaims,
210
+ accessToken,
211
+ refreshToken: ref<string | null>(null),
212
+ accessTokenExpired,
213
+ accessTokenExpirationDate,
214
+ init: async () => Promise.resolve(),
215
+ subscribeToEvent: noopSubscription,
216
+ subscribeToAllEvents: noopSubscription,
217
+ checkAuthentication: async () => Promise.resolve(isAuthenticated.value),
218
+ tokenExchange: browserOnly('tokenExchange'),
219
+ handleCallback: browserOnly('handleCallback'),
220
+ refresh: browserOnly('refresh'),
221
+ revoke: browserOnly('revoke'),
222
+ logout: browserOnly('logout'),
223
+ login: browserOnly('login'),
224
+ register: browserOnly('register'),
225
+ entry: browserOnly('entry'),
226
+ };
227
+ }
228
+
229
+ /**
230
+ * A composable to access the Strivacity SDK context.
231
+ *
232
+ * In the browser it returns the live SDK instance and its reactive state. During server-side rendering it returns a
233
+ * read-only context derived from the current request's session: the refs and `sdk.options` / `sdk.session` can be read,
234
+ * while SDK methods such as `login()` throw, because authentication flows only run in the browser.
235
+ *
236
+ * @template T - The type of the Strivacity SDK flow (RedirectFlow, PopupFlow, NativeFlow, or EmbeddedFlow).
237
+ * @returns {SDKContext<T>} The Strivacity SDK context.
238
+ */
239
+ export const useStrivacity = <T extends Flow = Flow>(): SDKContext<T> => {
240
+ if (import.meta.server || tryUseNuxtApp()?.ssrContext) {
241
+ return createServerContext() as unknown as SDKContext<T>;
242
+ }
243
+
244
+ return getClientContext() as unknown as SDKContext<T>;
95
245
  };
@@ -1,4 +1,6 @@
1
+ import { appendResponseHeader } from 'h3';
1
2
  import { defineNuxtRouteMiddleware, useNuxtApp } from '#imports';
3
+ import { flushSetCookies, toClientSession } from '@strivacity/sdk-core/utils';
2
4
  import { useSession } from '../composables/use-session';
3
5
 
4
6
  export default defineNuxtRouteMiddleware(async () => {
@@ -12,5 +14,12 @@ export default defineNuxtRouteMiddleware(async () => {
12
14
  const { useStrivacity } = await import('../server/composables/use-strivacity');
13
15
  const sdk = useStrivacity(h3Event);
14
16
 
15
- useSession().value = (await sdk.getSession()) ?? undefined;
17
+ const session = await sdk.getSession();
18
+ const cookies = flushSetCookies(h3Event, new Response(null)).headers.getSetCookie();
19
+
20
+ for (const cookie of cookies) {
21
+ appendResponseHeader(h3Event, 'set-cookie', cookie);
22
+ }
23
+
24
+ useSession().value = toClientSession(session) ?? undefined;
16
25
  });
@@ -0,0 +1,17 @@
1
+ import { defineNuxtPlugin } from '#app';
2
+ import { getClientContext } from '../composables/use-strivacity';
3
+
4
+ /**
5
+ * Creates the browser SDK instance as soon as the Nuxt app starts on the client (it is never created during SSR), and
6
+ * exposes it as `useNuxtApp().$strivacity`. `useStrivacity()` returns the same context.
7
+ */
8
+ export default defineNuxtPlugin({
9
+ name: 'strivacity',
10
+ setup() {
11
+ return {
12
+ provide: {
13
+ strivacity: getClientContext(),
14
+ },
15
+ };
16
+ },
17
+ });
@@ -1,7 +1,7 @@
1
1
  import type { H3Event } from 'h3';
2
2
  import type { NitroApp } from 'nitropack/types';
3
3
  import type { NuxtServerSDK, NuxtServerSDKInitConfig } from '../../types';
4
- import { toWebRequest } from 'h3';
4
+ import { toWebRequest, createError } from 'h3';
5
5
  import { useRuntimeConfig } from '#imports';
6
6
  import { defineNitroPlugin } from 'nitropack/dist/runtime/plugin';
7
7
  import { createBaseServerSDK } from '@strivacity/sdk-core/server';
@@ -16,11 +16,72 @@ declare module 'h3' {
16
16
  }
17
17
  }
18
18
 
19
- export default defineNitroPlugin((nitroApp) => {
20
- void setupAuthPlugin(nitroApp);
19
+ type BaseSDK = ReturnType<typeof createBaseServerSDK<H3Event>>;
20
+
21
+ export default defineNitroPlugin((nitroApp: NitroApp) => {
22
+ let base: BaseSDK | undefined;
23
+ let setupError: unknown;
24
+
25
+ const ready = setupAuthPlugin()
26
+ .then((result) => {
27
+ base = result;
28
+ })
29
+ .catch((error: unknown) => {
30
+ // NOTE: Nitro calls plugins synchronously and never awaits or catches their result, so a rejection
31
+ // here would otherwise be a silent unhandled rejection. Logging it and falling back to a stub sdk
32
+ // below turns every request into a clear 500 instead of an unrelated "sdk is undefined" error.
33
+ // eslint-disable-next-line no-console
34
+ console.error('[@strivacity/sdk-nuxt] Failed to initialize the Strivacity server SDK:', error);
35
+ setupError = error;
36
+ });
37
+
38
+ // NOTE: Registered synchronously, before setupAuthPlugin's awaited dynamic imports resolve, so even the
39
+ // very first request right after startup is covered by it - the hook itself just awaits `ready` first.
40
+ nitroApp.hooks.hook('request', async (event: H3Event) => {
41
+ await ready;
42
+
43
+ event.context.strivacity ??= {} as never;
44
+
45
+ if (event.context.strivacity.sdk) {
46
+ return;
47
+ }
48
+
49
+ if (!base) {
50
+ event.context.strivacity.sdk = new Proxy({} as NuxtServerSDK, {
51
+ get: () => {
52
+ throw createError({ statusCode: 500, statusMessage: 'Strivacity SDK failed to initialize', cause: setupError });
53
+ },
54
+ });
55
+ return;
56
+ }
57
+
58
+ const sdk = base;
59
+
60
+ event.context.strivacity.sdk = {
61
+ get options() {
62
+ return sdk.options;
63
+ },
64
+ getSession: (_event, params) => sdk.getSession(event, params),
65
+ updateSession: (session) => sdk.updateSession(session, event),
66
+ refreshSession: () => sdk.refreshSession(event),
67
+ revokeSession: () => sdk.revokeSession(event),
68
+ getEntrySession: (entryUrl) => sdk.getEntrySession(entryUrl),
69
+ completeLogin: (params) => sdk.completeLogin(params, event),
70
+ logout: (postLogoutRedirectUri) => sdk.logout(postLogoutRedirectUri, event),
71
+ handleLogin: (evt) => sdk.handleLogin(evt),
72
+ handleRegister: (evt) => sdk.handleRegister(evt),
73
+ handleCallback: (evt) => sdk.handleCallback(evt),
74
+ handleRefresh: (evt) => sdk.handleRefresh(evt),
75
+ handleRevoke: (evt) => sdk.handleRevoke(evt),
76
+ handleEntry: (evt) => sdk.handleEntry(evt),
77
+ handleLogout: (evt) => sdk.handleLogout(evt),
78
+ handleBackChannelLogout: (evt) => sdk.handleBackChannelLogout(evt),
79
+ handler: (evt) => sdk.handler(evt),
80
+ };
81
+ });
21
82
  });
22
83
 
23
- async function setupAuthPlugin(nitroApp: NitroApp) {
84
+ async function setupAuthPlugin(): Promise<BaseSDK> {
24
85
  const config = useRuntimeConfig();
25
86
  const initConfig = { ...config.strivacity } as NuxtServerSDKInitConfig;
26
87
  const customStorage = (await import('#strivacity-options-storage')).default();
@@ -50,35 +111,10 @@ async function setupAuthPlugin(nitroApp: NitroApp) {
50
111
  initConfig.storage = createEncryptedCookieStorage(initConfig.secret, { defaultCookieOptions: { maxAge: initConfig.cookieMaxAge } });
51
112
  }
52
113
 
53
- const base = createBaseServerSDK<H3Event>(
114
+ return createBaseServerSDK<H3Event>(
54
115
  {
55
116
  toRequest: (event) => toWebRequest(event),
56
117
  },
57
118
  initConfig,
58
119
  );
59
-
60
- nitroApp.hooks.hook('request', (event: H3Event) => {
61
- event.context.strivacity ??= {} as never;
62
- event.context.strivacity.sdk ??= {
63
- get options() {
64
- return base.options;
65
- },
66
- getSession: () => base.getSession(event),
67
- updateSession: (session) => base.updateSession(session, event),
68
- refreshSession: () => base.refreshSession(event),
69
- revokeSession: () => base.revokeSession(event),
70
- getEntrySession: (entryUrl) => base.getEntrySession(entryUrl),
71
- completeLogin: (params) => base.completeLogin(params, event),
72
- logout: (postLogoutRedirectUri) => base.logout(postLogoutRedirectUri, event),
73
- handleLogin: (evt) => base.handleLogin(evt),
74
- handleRegister: (evt) => base.handleRegister(evt),
75
- handleCallback: (evt) => base.handleCallback(evt),
76
- handleRefresh: (evt) => base.handleRefresh(evt),
77
- handleRevoke: (evt) => base.handleRevoke(evt),
78
- handleEntry: (evt) => base.handleEntry(evt),
79
- handleLogout: (evt) => base.handleLogout(evt),
80
- handleBackChannelLogout: (evt) => base.handleBackChannelLogout(evt),
81
- handler: (evt) => base.handler(evt),
82
- };
83
- });
84
120
  }