@strivacity/sdk-svelte 3.0.2 → 4.0.0-beta.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 (107) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1714 -428
  3. package/dist/assets/rolldown-runtime.cjs +1 -0
  4. package/dist/client/components.svelte.cjs +2 -0
  5. package/dist/client/components.svelte.cjs.map +1 -0
  6. package/dist/client/components.svelte.d.ts +11 -0
  7. package/dist/client/components.svelte.mjs +2 -0
  8. package/dist/client/components.svelte.mjs.map +1 -0
  9. package/dist/client/hooks.svelte.cjs +2 -0
  10. package/dist/client/hooks.svelte.cjs.map +1 -0
  11. package/dist/client/hooks.svelte.d.ts +27 -0
  12. package/dist/client/hooks.svelte.mjs +2 -0
  13. package/dist/client/hooks.svelte.mjs.map +1 -0
  14. package/dist/client/index.cjs +1 -0
  15. package/dist/client/index.d.ts +8 -0
  16. package/dist/client/index.mjs +1 -0
  17. package/dist/client/storages.cjs +1 -0
  18. package/dist/client/storages.d.ts +1 -0
  19. package/dist/client/storages.mjs +1 -0
  20. package/dist/client/types.cjs +1 -0
  21. package/dist/client/types.d.ts +238 -0
  22. package/dist/client/types.mjs +1 -0
  23. package/dist/client/utils.cjs +1 -0
  24. package/dist/client/utils.d.ts +1 -0
  25. package/dist/client/utils.mjs +1 -0
  26. package/dist/errors.cjs +1 -0
  27. package/dist/errors.d.ts +1 -0
  28. package/dist/errors.mjs +1 -0
  29. package/dist/server/index.cjs +1 -0
  30. package/dist/server/index.d.ts +6 -0
  31. package/dist/server/index.mjs +1 -0
  32. package/dist/server/sdk.cjs +2 -0
  33. package/dist/server/sdk.cjs.map +1 -0
  34. package/dist/server/sdk.d.ts +8 -0
  35. package/dist/server/sdk.mjs +2 -0
  36. package/dist/server/sdk.mjs.map +1 -0
  37. package/dist/server/storages.cjs +2 -0
  38. package/dist/server/storages.cjs.map +1 -0
  39. package/dist/server/storages.d.ts +10 -0
  40. package/dist/server/storages.mjs +2 -0
  41. package/dist/server/storages.mjs.map +1 -0
  42. package/dist/server/types.cjs +1 -0
  43. package/dist/server/types.d.ts +35 -0
  44. package/dist/server/types.mjs +1 -0
  45. package/dist/server/utils.cjs +1 -0
  46. package/dist/server/utils.d.ts +1 -0
  47. package/dist/server/utils.mjs +1 -0
  48. package/dist/types.cjs +1 -2
  49. package/dist/types.d.ts +2 -181
  50. package/dist/types.mjs +1 -2
  51. package/eslint.config.mjs +3 -0
  52. package/package.json +59 -11
  53. package/project.json +47 -0
  54. package/src/client/components.svelte.ts +125 -0
  55. package/src/client/hooks.svelte.ts +221 -0
  56. package/src/client/index.ts +8 -0
  57. package/src/client/storages.ts +1 -0
  58. package/src/client/types.ts +299 -0
  59. package/src/client/utils.ts +1 -0
  60. package/src/errors.ts +1 -0
  61. package/src/server/index.ts +6 -0
  62. package/src/server/sdk.ts +77 -0
  63. package/src/server/storages.ts +22 -0
  64. package/src/server/types.ts +49 -0
  65. package/src/server/utils.ts +1 -0
  66. package/src/types.ts +2 -0
  67. package/svelte.config.js +5 -0
  68. package/testing/fixtures/AuthProviderHost.svelte +29 -0
  69. package/testing/fixtures/Harness.svelte +30 -0
  70. package/testing/fixtures/NativeLoginConsumer.svelte +21 -0
  71. package/testing/fixtures/NativeLoginContextConsumer.svelte +10 -0
  72. package/testing/fixtures/StrivacityConsumer.svelte +10 -0
  73. package/testing/tests/components.spec.ts +183 -0
  74. package/testing/tests/errors.spec.ts +10 -0
  75. package/testing/tests/hooks.spec.ts +275 -0
  76. package/testing/tests/index.spec.ts +199 -0
  77. package/testing/tests/server/sdk.spec.ts +214 -0
  78. package/testing/tests/server/storages.spec.ts +50 -0
  79. package/testing/tests/server/utils.spec.ts +10 -0
  80. package/testing/tests/storages.spec.ts +10 -0
  81. package/testing/tests/utils.spec.ts +10 -0
  82. package/testing/utils/mount.ts +83 -0
  83. package/tsconfig.app.json +4 -0
  84. package/tsconfig.json +6 -0
  85. package/vite.config.mts +52 -0
  86. package/dist/AuthProvider.cjs +0 -2
  87. package/dist/AuthProvider.cjs.map +0 -1
  88. package/dist/AuthProvider.mjs +0 -2
  89. package/dist/AuthProvider.mjs.map +0 -1
  90. package/dist/AuthProvider.svelte.d.ts +0 -1
  91. package/dist/LoginRenderer.cjs +0 -2
  92. package/dist/LoginRenderer.cjs.map +0 -1
  93. package/dist/LoginRenderer.mjs +0 -2
  94. package/dist/LoginRenderer.mjs.map +0 -1
  95. package/dist/LoginRenderer.svelte.d.ts +0 -1
  96. package/dist/composables.cjs +0 -2
  97. package/dist/composables.cjs.map +0 -1
  98. package/dist/composables.d.ts +0 -12
  99. package/dist/composables.mjs +0 -2
  100. package/dist/composables.mjs.map +0 -1
  101. package/dist/index.cjs +0 -2
  102. package/dist/index.cjs.map +0 -1
  103. package/dist/index.d.ts +0 -13
  104. package/dist/index.mjs +0 -2
  105. package/dist/index.mjs.map +0 -1
  106. package/dist/types.cjs.map +0 -1
  107. package/dist/types.mjs.map +0 -1
@@ -0,0 +1,183 @@
1
+ import type { StyAuthProviderProps, RedirectFlow } from '../../src/types';
2
+ import { describe, test, expect, vi, beforeEach } from 'vitest';
3
+ import { initFlow } from '@strivacity/sdk-core';
4
+ import { flushPromises } from '@strivacity/testing/mocks/common';
5
+ import { createMockFlow } from '@strivacity/testing/mocks/sdk';
6
+ import { mountAuthProvider, settle, unmount } from '../utils/mount';
7
+
8
+ vi.mock('@strivacity/sdk-core', async (importOriginal) => ({
9
+ ...(await importOriginal<typeof import('@strivacity/sdk-core')>()),
10
+ initFlow: vi.fn(),
11
+ }));
12
+
13
+ const options: StyAuthProviderProps['options'] = {
14
+ mode: 'redirect',
15
+ issuer: 'https://brandtegrity.io',
16
+ clientId: 'client-id',
17
+ redirectUri: 'https://brandtegrity.io/callback',
18
+ };
19
+
20
+ describe('createStyAuthProvider', () => {
21
+ beforeEach(() => {
22
+ vi.mocked(initFlow).mockReset();
23
+ });
24
+
25
+ test('initializes the underlying flow through core sdk initFlow with the given options', async () => {
26
+ const flow = createMockFlow<RedirectFlow>();
27
+ vi.mocked(initFlow).mockReturnValue(flow as never);
28
+
29
+ await mountAuthProvider(options);
30
+
31
+ expect(initFlow).toHaveBeenCalledTimes(1);
32
+ expect(initFlow).toHaveBeenCalledWith(options);
33
+ });
34
+
35
+ test('subscribes to all sdk events on mount and disposes the subscription on unmount', async () => {
36
+ const dispose = vi.fn();
37
+ const flow = createMockFlow<RedirectFlow>({ subscribeToAllEvents: vi.fn().mockReturnValue({ dispose }) });
38
+ vi.mocked(initFlow).mockReturnValue(flow as never);
39
+
40
+ const { instance } = await mountAuthProvider(options);
41
+
42
+ expect(flow.subscribeToAllEvents).toHaveBeenCalledTimes(1);
43
+ expect(flow.subscribeToAllEvents).toHaveBeenCalledWith(expect.any(Function));
44
+
45
+ await unmount(instance as never);
46
+
47
+ expect(dispose).toHaveBeenCalledTimes(1);
48
+ });
49
+
50
+ test('provides its context to descendant components via useStrivacity', async () => {
51
+ const flow = createMockFlow<RedirectFlow>();
52
+ vi.mocked(initFlow).mockReturnValue(flow as never);
53
+
54
+ const { instance } = await mountAuthProvider(options);
55
+
56
+ expect(instance.getChildContextValue()?.sdk).toBe(flow);
57
+ });
58
+
59
+ test.each([
60
+ ['init', [], undefined],
61
+ ['subscribeToEvent', ['loggedIn', vi.fn()], { dispose: vi.fn() }],
62
+ ['checkAuthentication', [{ autoRefresh: false }], true],
63
+ ['tokenExchange', [{ code: 'abc' }], undefined],
64
+ ['handleCallback', ['https://brandtegrity.io/callback?code=abc'], undefined],
65
+ ['refresh', [], undefined],
66
+ ['revoke', [], undefined],
67
+ ['logout', [{ postLogoutRedirectUri: 'https://brandtegrity.io' }], undefined],
68
+ ['login', [{ scopes: ['openid'] }], undefined],
69
+ ['register', [{ scopes: ['openid'] }], undefined],
70
+ ['entry', ['https://brandtegrity.io/entry'], undefined],
71
+ ] as const)('%s calls through to the flow instance returned by core sdk', async (method, args, resolvedValue) => {
72
+ const flow = createMockFlow<RedirectFlow>({ [method]: vi.fn().mockReturnValue(resolvedValue) });
73
+ vi.mocked(initFlow).mockReturnValue(flow as never);
74
+
75
+ const { instance } = await mountAuthProvider(options);
76
+ const context = instance.getContextValue();
77
+
78
+ const result = await (context[method] as (...a: Array<unknown>) => unknown)(...args);
79
+
80
+ expect(flow[method as keyof RedirectFlow]).toHaveBeenCalledWith(...args);
81
+ expect(result).toEqual(resolvedValue);
82
+ });
83
+
84
+ test('subscribeToAllEvents on the context calls through to the flow instance', async () => {
85
+ const callback = vi.fn();
86
+ const disposeResult = { dispose: vi.fn() };
87
+ const flow = createMockFlow<RedirectFlow>();
88
+ vi.mocked(flow.subscribeToAllEvents).mockReturnValue(disposeResult);
89
+ vi.mocked(initFlow).mockReturnValue(flow as never);
90
+
91
+ const { instance } = await mountAuthProvider(options);
92
+ // the mount-time subscription happens first; this is the caller-facing pass-through
93
+ const result = instance.getContextValue().subscribeToAllEvents(callback);
94
+
95
+ expect(flow.subscribeToAllEvents).toHaveBeenLastCalledWith(callback);
96
+ expect(result).toBe(disposeResult);
97
+ });
98
+
99
+ test('exposes reactive state seeded from the flow instance after the initial session update settles', async () => {
100
+ const flow = createMockFlow<RedirectFlow>({
101
+ isAuthenticated: Promise.resolve(true),
102
+ language: 'hu-HU',
103
+ idTokenClaims: { sub: 'user-1' },
104
+ accessToken: 'access-token',
105
+ refreshToken: 'refresh-token',
106
+ accessTokenExpired: false,
107
+ accessTokenExpirationDate: 1234,
108
+ });
109
+ vi.mocked(initFlow).mockReturnValue(flow as never);
110
+
111
+ const { instance } = await mountAuthProvider(options);
112
+ const context = instance.getContextValue();
113
+
114
+ expect(context.loading).toBe(false);
115
+ expect(context.isAuthenticated).toBe(true);
116
+ expect(context.language).toBe('hu-HU');
117
+ expect(context.idTokenClaims).toEqual({ sub: 'user-1' });
118
+ expect(context.accessToken).toBe('access-token');
119
+ expect(context.refreshToken).toBe('refresh-token');
120
+ expect(context.accessTokenExpired).toBe(false);
121
+ expect(context.accessTokenExpirationDate).toBe(1234);
122
+ });
123
+
124
+ test('reactive state updates when the flow emits an event', async () => {
125
+ const subscribeToAllEvents = vi.fn().mockReturnValue({ dispose: vi.fn() });
126
+ const flow = createMockFlow<RedirectFlow>({ subscribeToAllEvents });
127
+ vi.mocked(initFlow).mockReturnValue(flow as never);
128
+
129
+ const { instance } = await mountAuthProvider(options);
130
+ const context = instance.getContextValue();
131
+
132
+ const onEvent = subscribeToAllEvents.mock.calls[0][0] as () => Promise<void>;
133
+ const mutableFlow = flow as unknown as { language: string; accessToken: string | null; isAuthenticated: Promise<boolean> };
134
+
135
+ mutableFlow.language = 'de-DE';
136
+ mutableFlow.accessToken = 'new-token';
137
+ mutableFlow.isAuthenticated = Promise.resolve(true);
138
+
139
+ await onEvent();
140
+ await flushPromises();
141
+
142
+ expect(context.language).toBe('de-DE');
143
+ expect(context.accessToken).toBe('new-token');
144
+ expect(context.isAuthenticated).toBe(true);
145
+ });
146
+
147
+ describe('session seeding', () => {
148
+ test('assigns the given session to the flow instance once known', async () => {
149
+ const flow = createMockFlow<RedirectFlow>();
150
+ vi.mocked(initFlow).mockReturnValue(flow as never);
151
+ const session = { access_token: 'access-token' } as never;
152
+
153
+ await mountAuthProvider(options, session);
154
+
155
+ // $state proxies assigned objects, so the flow ends up holding a reactive proxy of `session` rather than
156
+ // the exact same reference; compare by value instead.
157
+ expect(flow.session).toEqual(session);
158
+ });
159
+
160
+ test('reacts to the session getter changing after mount', async () => {
161
+ const flow = createMockFlow<RedirectFlow>();
162
+ vi.mocked(initFlow).mockReturnValue(flow as never);
163
+
164
+ const { instance } = await mountAuthProvider(options, null);
165
+ expect(flow.session).toBeNull();
166
+
167
+ const newSession = { access_token: 'new-access-token' } as never;
168
+ instance.setSession(newSession);
169
+ await settle();
170
+
171
+ expect(flow.session).toEqual(newSession);
172
+ });
173
+
174
+ test('does not assign a falsy session to the flow instance', async () => {
175
+ const flow = createMockFlow<RedirectFlow>();
176
+ vi.mocked(initFlow).mockReturnValue(flow as never);
177
+
178
+ await mountAuthProvider(options, null);
179
+
180
+ expect(flow.session).toBeNull();
181
+ });
182
+ });
183
+ });
@@ -0,0 +1,10 @@
1
+ import { test, expect } from 'vitest';
2
+ import * as svelteErrors from '../../src/errors';
3
+ import * as coreErrors from '@strivacity/sdk-core/utils/errors';
4
+
5
+ test('re-exports error classes from the core sdk', () => {
6
+ const coreKeys = Object.keys(coreErrors);
7
+
8
+ expect(coreKeys.length).toBeGreaterThan(0);
9
+ expect(Object.keys(svelteErrors).sort()).toEqual(coreKeys.sort());
10
+ });
@@ -0,0 +1,275 @@
1
+ import type { SDKContext, NativeFlow, NativeFlowState, SessionData } from '../../src/types';
2
+ import { describe, test, expect, vi } from 'vitest';
3
+ import { FallbackError } from '@strivacity/sdk-core/utils/errors';
4
+ import { flushPromises } from '@strivacity/testing/mocks/common';
5
+ import { createMockFlow } from '@strivacity/testing/mocks/sdk';
6
+ import {
7
+ mountStrivacityConsumer,
8
+ mountStrivacityConsumerWithoutProvider,
9
+ mountNativeLoginContextConsumerWithoutProvider,
10
+ mountWithNativeLogin,
11
+ } from '../utils/mount';
12
+
13
+ describe('useStrivacity', () => {
14
+ test('throws when used outside of a Strivacity SDK provider', () => {
15
+ expect(() => {
16
+ mountStrivacityConsumerWithoutProvider();
17
+ }).toThrow('Missing SDK context');
18
+ });
19
+
20
+ test('resolves to the context provided by an ancestor createStyAuthProvider/setContext call', async () => {
21
+ const sdk = {} as NativeFlow;
22
+ const context = { sdk } as unknown as SDKContext<NativeFlow>;
23
+
24
+ const { instance } = await mountStrivacityConsumer(context);
25
+
26
+ expect(instance.getContextValue().sdk).toBe(sdk);
27
+ });
28
+ });
29
+
30
+ describe('useNativeLoginContext', () => {
31
+ test('throws when used outside of a native login session', () => {
32
+ expect(() => {
33
+ mountNativeLoginContextConsumerWithoutProvider();
34
+ }).toThrow('Missing SDK native login context');
35
+ });
36
+
37
+ test('resolves to the same context object provided by an ancestor useNativeLogin call', async () => {
38
+ const sdk = createMockFlow<NativeFlow>();
39
+ const { login, getChildContext } = await mountWithNativeLogin(sdk);
40
+
41
+ const injected = getChildContext();
42
+
43
+ expect(injected?.state).toBe(login.state);
44
+ expect(injected?.loading).toBe(login.loading);
45
+ expect(injected?.forms).toBe(login.forms);
46
+ expect(injected?.messages).toBe(login.messages);
47
+ expect(injected?.submitForm).toBe(login.submitForm);
48
+ });
49
+ });
50
+
51
+ describe('useNativeLogin', () => {
52
+ test('initializes the sdk and starts a session on mount, defaulting params to an empty object', async () => {
53
+ const sdk = createMockFlow<NativeFlow>();
54
+ await mountWithNativeLogin(sdk);
55
+
56
+ expect(sdk.init).toHaveBeenCalledTimes(1);
57
+ expect(sdk.startSession).toHaveBeenCalledWith({});
58
+ });
59
+
60
+ test('forwards the given login params to sdk.startSession', async () => {
61
+ const sdk = createMockFlow<NativeFlow>();
62
+ await mountWithNativeLogin(sdk, { params: { sessionId: 'session-1', language: 'hu-HU' } });
63
+
64
+ expect(sdk.startSession).toHaveBeenCalledWith({ sessionId: 'session-1', language: 'hu-HU' });
65
+ });
66
+
67
+ test('is loading until sdk.startSession resolves with a state that has no pending finalizeUrl', async () => {
68
+ const sdk = createMockFlow<NativeFlow>();
69
+ vi.mocked(sdk.startSession).mockResolvedValue({ screen: 'identifier' });
70
+ const { login } = await mountWithNativeLogin(sdk);
71
+
72
+ expect(login.loading).toBe(false);
73
+ });
74
+
75
+ test('stays loading forever if sdk.startSession resolves with no state at all', async () => {
76
+ const sdk = createMockFlow<NativeFlow>();
77
+ vi.mocked(sdk.startSession).mockResolvedValue(undefined);
78
+ const { login } = await mountWithNativeLogin(sdk);
79
+
80
+ expect(login.loading).toBe(true);
81
+ });
82
+
83
+ test('applies the returned flow state: resets per-form data, routes the global message, and keeps per-widget messages', async () => {
84
+ const state: NativeFlowState = {
85
+ hostedUrl: 'https://brandtegrity.io/hosted',
86
+ screen: 'identifier',
87
+ forms: [{ id: 'form1', type: 'form', widgets: [] }],
88
+ messages: {
89
+ global: { type: 'info', text: 'Welcome' },
90
+ form1: { field1: { type: 'error', text: 'Required' } },
91
+ },
92
+ };
93
+ const sdk = createMockFlow<NativeFlow>();
94
+ vi.mocked(sdk.startSession).mockResolvedValue(state);
95
+ const onGlobalMessage = vi.fn();
96
+
97
+ const { login } = await mountWithNativeLogin(sdk, { onGlobalMessage });
98
+
99
+ expect(onGlobalMessage).toHaveBeenCalledWith({ type: 'info', text: 'Welcome' });
100
+ expect(login.forms).toEqual({ form1: {} });
101
+ expect(login.messages).toEqual({ form1: { field1: { type: 'error', text: 'Required' } } });
102
+ expect(login.state).toEqual(state);
103
+ });
104
+
105
+ test('keeps loading while a finalizeUrl is still pending', async () => {
106
+ const sdk = createMockFlow<NativeFlow>();
107
+ vi.mocked(sdk.startSession).mockResolvedValue({ finalizeUrl: 'https://brandtegrity.io/finalize' });
108
+
109
+ const { login } = await mountWithNativeLogin(sdk);
110
+
111
+ expect(login.loading).toBe(true);
112
+ });
113
+
114
+ test('calls onLogin and leaves the tracked state untouched when the sdk already has a session', async () => {
115
+ const session = { access_token: 'access-token' } as unknown as SessionData;
116
+ const sdk = createMockFlow<NativeFlow>({ session });
117
+ vi.mocked(sdk.startSession).mockResolvedValue({ screen: 'identifier' });
118
+ const onLogin = vi.fn();
119
+
120
+ const { login } = await mountWithNativeLogin(sdk, { onLogin });
121
+
122
+ expect(onLogin).toHaveBeenCalledWith(session);
123
+ expect(login.state).toEqual({});
124
+ });
125
+
126
+ test('routes a FallbackError from sdk.startSession to onFallback and logs it', async () => {
127
+ const sdk = createMockFlow<NativeFlow>();
128
+ const fallbackError = new FallbackError(new URL('https://brandtegrity.io/hosted'));
129
+ vi.mocked(sdk.startSession).mockRejectedValue(fallbackError);
130
+ const onFallback = vi.fn();
131
+
132
+ const { login } = await mountWithNativeLogin(sdk, { onFallback });
133
+
134
+ expect(onFallback).toHaveBeenCalledWith(fallbackError);
135
+ expect(sdk.logging!.error).toHaveBeenCalledWith('Fallback error occurred', fallbackError);
136
+ expect(login.loading).toBe(true);
137
+ });
138
+
139
+ test('routes a non-fallback error from sdk.startSession to onError and logs it', async () => {
140
+ const sdk = createMockFlow<NativeFlow>();
141
+ const error = new Error('network down');
142
+ vi.mocked(sdk.startSession).mockRejectedValue(error);
143
+ const onError = vi.fn();
144
+
145
+ await mountWithNativeLogin(sdk, { onError });
146
+
147
+ expect(onError).toHaveBeenCalledWith(error);
148
+ expect(sdk.logging!.error).toHaveBeenCalledWith('Error starting session', error);
149
+ });
150
+
151
+ describe('submitForm', () => {
152
+ test('unflattens the tracked form values and calls through to sdk.submitForm', async () => {
153
+ const sdk = createMockFlow<NativeFlow>();
154
+ const initialForms = [{ id: 'form1', type: 'form' as const, widgets: [] }];
155
+ vi.mocked(sdk.startSession).mockResolvedValue({ screen: 'identifier', forms: initialForms });
156
+ vi.mocked(sdk.submitForm).mockResolvedValue({ screen: 'done' });
157
+
158
+ const { login } = await mountWithNativeLogin(sdk);
159
+
160
+ login.setFormValue('form1', 'address.city', 'Budapest');
161
+ login.setFormValue('form1', 'address.zip', '');
162
+
163
+ await login.submitForm('form1');
164
+ await flushPromises();
165
+
166
+ expect(sdk.submitForm).toHaveBeenCalledWith('form1', { address: { city: 'Budapest', zip: null } });
167
+ // unspecified fields on the new state fall back to what was already tracked (except messages, which always resets)
168
+ expect(login.state).toEqual({
169
+ hostedUrl: undefined,
170
+ finalizeUrl: undefined,
171
+ screen: 'done',
172
+ forms: initialForms,
173
+ layout: undefined,
174
+ messages: {},
175
+ branding: undefined,
176
+ });
177
+ });
178
+
179
+ test('uses a given custom body instead of the tracked form values', async () => {
180
+ const sdk = createMockFlow<NativeFlow>();
181
+ vi.mocked(sdk.submitForm).mockResolvedValue({ screen: 'done' });
182
+
183
+ const { login } = await mountWithNativeLogin(sdk);
184
+
185
+ await login.submitForm('form1', { raw: true });
186
+
187
+ expect(sdk.submitForm).toHaveBeenCalledWith('form1', { raw: true });
188
+ });
189
+
190
+ test('routes a FallbackError to onFallback', async () => {
191
+ const sdk = createMockFlow<NativeFlow>();
192
+ const fallbackError = new FallbackError(new URL('https://brandtegrity.io/hosted'));
193
+ vi.mocked(sdk.submitForm).mockRejectedValue(fallbackError);
194
+ const onFallback = vi.fn();
195
+
196
+ const { login } = await mountWithNativeLogin(sdk, { onFallback });
197
+
198
+ await login.submitForm('form1');
199
+
200
+ expect(onFallback).toHaveBeenCalledWith(fallbackError);
201
+ expect(sdk.logging!.error).toHaveBeenCalledWith('Fallback error occurred', fallbackError);
202
+ });
203
+
204
+ test('routes a non-fallback error to onError', async () => {
205
+ const sdk = createMockFlow<NativeFlow>();
206
+ const error = new Error('submit failed');
207
+ vi.mocked(sdk.submitForm).mockRejectedValue(error);
208
+ const onError = vi.fn();
209
+
210
+ const { login } = await mountWithNativeLogin(sdk, { onError });
211
+
212
+ await login.submitForm('form1');
213
+
214
+ expect(onError).toHaveBeenCalledWith(error);
215
+ expect(sdk.logging!.error).toHaveBeenCalledWith('Error submitting form', error);
216
+ });
217
+ });
218
+
219
+ test('setFormValue coerces empty strings to null and lazily creates the form bucket', async () => {
220
+ const sdk = createMockFlow<NativeFlow>();
221
+ const { login } = await mountWithNativeLogin(sdk);
222
+
223
+ login.setFormValue('newForm', 'field', 'value');
224
+ login.setFormValue('newForm', 'other', '');
225
+
226
+ expect(login.forms.newForm).toEqual({ field: 'value', other: null });
227
+ });
228
+
229
+ test('setMessage lazily creates the message bucket for a form', async () => {
230
+ const sdk = createMockFlow<NativeFlow>();
231
+ const { login } = await mountWithNativeLogin(sdk);
232
+
233
+ login.setMessage('newForm', 'field', { type: 'error', text: 'Required' });
234
+
235
+ expect(login.messages.newForm).toEqual({ field: { type: 'error', text: 'Required' } });
236
+ });
237
+
238
+ describe('triggerFallback', () => {
239
+ test('throws when no hosted URL is known yet', async () => {
240
+ const sdk = createMockFlow<NativeFlow>();
241
+ vi.mocked(sdk.startSession).mockResolvedValue({ screen: 'identifier' });
242
+ const { login } = await mountWithNativeLogin(sdk);
243
+
244
+ expect(() => login.triggerFallback()).toThrow('No hosted URL provided');
245
+ expect(sdk.logging!.error).toHaveBeenCalledWith('Fallback error', expect.any(Error));
246
+ });
247
+
248
+ test('calls onFallback with a FallbackError built from the known hosted URL', async () => {
249
+ const sdk = createMockFlow<NativeFlow>();
250
+ vi.mocked(sdk.startSession).mockResolvedValue({ hostedUrl: 'https://brandtegrity.io/hosted', screen: 'identifier' });
251
+ const onFallback = vi.fn();
252
+
253
+ const { login } = await mountWithNativeLogin(sdk, { onFallback });
254
+
255
+ login.triggerFallback('manual trigger');
256
+
257
+ expect(sdk.logging!.warn).toHaveBeenCalledWith('Triggering fallback due to: manual trigger');
258
+ expect(onFallback).toHaveBeenCalledWith(expect.any(FallbackError));
259
+ // triggerFallback builds the FallbackError's url from svelte/reactivity's SvelteURL rather than the plain URL constructor;
260
+ // it still compares equal to a plain URL through its string form.
261
+ expect((onFallback.mock.calls[0][0] as FallbackError).url.toString()).toBe('https://brandtegrity.io/hosted');
262
+ });
263
+ });
264
+
265
+ test('triggerClose calls onClose and logs', async () => {
266
+ const sdk = createMockFlow<NativeFlow>();
267
+ const onClose = vi.fn();
268
+ const { login } = await mountWithNativeLogin(sdk, { onClose });
269
+
270
+ login.triggerClose();
271
+
272
+ expect(onClose).toHaveBeenCalledTimes(1);
273
+ expect(sdk.logging!.debug).toHaveBeenCalledWith('Triggering close');
274
+ });
275
+ });
@@ -0,0 +1,199 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import * as clientIndex from '../../src/client/index';
3
+ import * as serverIndex from '../../src/server/index';
4
+
5
+ describe('client', () => {
6
+ test('should export the correct things', () => {
7
+ const expectedExports = [
8
+ 'createBaseFlow',
9
+ 'createEmbeddedFlow',
10
+ 'createNativeFlow',
11
+ 'createPopupFlow',
12
+ 'createRedirectFlow',
13
+ 'popupCallbackHandler',
14
+ 'popupUrlHandler',
15
+ 'redirectCallbackHandler',
16
+ 'redirectUrlHandler',
17
+ 'BACKCHANNEL_LOGOUT_EVENT',
18
+ 'ConfigurationError',
19
+ 'DEFAULT_COOKIE_MAX_AGE',
20
+ 'DISALLOWED_PROXY_HEADERS',
21
+ 'FallbackError',
22
+ 'InternalError',
23
+ 'NetworkError',
24
+ 'OidcError',
25
+ 'PopupBlockedError',
26
+ 'PopupClosedError',
27
+ 'ProtocolError',
28
+ 'RETURN_TO_COOKIE',
29
+ 'ServerError',
30
+ 'SessionExpiredError',
31
+ 'UnsupportedFlowError',
32
+ 'assertWebAuthnCredential',
33
+ 'buildAuthorizationUrl',
34
+ 'buildCookieString',
35
+ 'buildEndSessionUrl',
36
+ 'createDefaultLogging',
37
+ 'createHttpClient',
38
+ 'createSession',
39
+ 'createState',
40
+ 'createWebAuthnCredential',
41
+ 'decodeBase64URL',
42
+ 'decodeJwt',
43
+ 'decryptString',
44
+ 'defaultOptions',
45
+ 'encodeBase64URL',
46
+ 'encryptString',
47
+ 'exchangeCode',
48
+ 'fetchFlowEntry',
49
+ 'finalizeLoginSession',
50
+ 'flushSetCookies',
51
+ 'generateCodeChallenge',
52
+ 'generateCodeVerifier',
53
+ 'generateJwt',
54
+ 'generateRandomHex',
55
+ 'getDefaultFlowState',
56
+ 'getDiscoveryDocument',
57
+ 'getSDKOptions',
58
+ 'getServerSDKOptions',
59
+ 'injectScript',
60
+ 'isSessionExpired',
61
+ 'loadSession',
62
+ 'parseCookieHeader',
63
+ 'parseState',
64
+ 'proxyResponse',
65
+ 'pushSetCookie',
66
+ 'refreshToken',
67
+ 'revokeToken',
68
+ 'serializeCookie',
69
+ 'serializeSession',
70
+ 'serializeState',
71
+ 'submitLoginForm',
72
+ 'throwHttpError',
73
+ 'throwTokenEndpointError',
74
+ 'timestamp',
75
+ 'toSafeRedirect',
76
+ 'unflattenObject',
77
+ 'verifyIdToken',
78
+ 'verifyJwt',
79
+ 'createStyAuthProvider',
80
+ 'STRIVACITY_SDK',
81
+ 'STRIVACITY_LOGIN_CONTEXT',
82
+ 'useStrivacity',
83
+ 'useNativeLoginContext',
84
+ 'useNativeLogin',
85
+ 'COOKIE_CHUNK_SIZE',
86
+ 'COOKIE_CONTEXT',
87
+ 'createCacheAPIStorage',
88
+ 'createEncryptedCookieStorage',
89
+ 'createIndexedDBStorage',
90
+ 'createLocalStorage',
91
+ 'createMemoryStorage',
92
+ 'createServerMemoryStorage',
93
+ 'createServerStateStorage',
94
+ 'createSessionIdCookieStorage',
95
+ 'createSessionStorage',
96
+ 'createWorkerStorage',
97
+ 'handleWorkerStorageRequests',
98
+ 'AlgorithmTypeList',
99
+ 'GrantTypeList',
100
+ 'ResponseModeList',
101
+ 'ResponseTypeList',
102
+ 'SubjectTypeList',
103
+ 'TokenEndpointAuthMethodList',
104
+ ];
105
+
106
+ expect(Object.keys(clientIndex)).toEqual(expectedExports);
107
+ });
108
+ });
109
+
110
+ describe('server', () => {
111
+ test('should export the correct things', () => {
112
+ const expectedExports = [
113
+ 'BACKCHANNEL_LOGOUT_EVENT',
114
+ 'ConfigurationError',
115
+ 'DEFAULT_COOKIE_MAX_AGE',
116
+ 'DISALLOWED_PROXY_HEADERS',
117
+ 'FallbackError',
118
+ 'InternalError',
119
+ 'NetworkError',
120
+ 'OidcError',
121
+ 'PopupBlockedError',
122
+ 'PopupClosedError',
123
+ 'ProtocolError',
124
+ 'RETURN_TO_COOKIE',
125
+ 'ServerError',
126
+ 'SessionExpiredError',
127
+ 'UnsupportedFlowError',
128
+ 'assertWebAuthnCredential',
129
+ 'buildAuthorizationUrl',
130
+ 'buildCookieString',
131
+ 'buildEndSessionUrl',
132
+ 'createDefaultLogging',
133
+ 'createHttpClient',
134
+ 'createSession',
135
+ 'createState',
136
+ 'createWebAuthnCredential',
137
+ 'decodeBase64URL',
138
+ 'decodeJwt',
139
+ 'decryptString',
140
+ 'defaultOptions',
141
+ 'encodeBase64URL',
142
+ 'encryptString',
143
+ 'exchangeCode',
144
+ 'fetchFlowEntry',
145
+ 'finalizeLoginSession',
146
+ 'flushSetCookies',
147
+ 'generateCodeChallenge',
148
+ 'generateCodeVerifier',
149
+ 'generateJwt',
150
+ 'generateRandomHex',
151
+ 'getDefaultFlowState',
152
+ 'getDiscoveryDocument',
153
+ 'getSDKOptions',
154
+ 'getServerSDKOptions',
155
+ 'injectScript',
156
+ 'isSessionExpired',
157
+ 'loadSession',
158
+ 'parseCookieHeader',
159
+ 'parseState',
160
+ 'proxyResponse',
161
+ 'pushSetCookie',
162
+ 'refreshToken',
163
+ 'revokeToken',
164
+ 'serializeCookie',
165
+ 'serializeSession',
166
+ 'serializeState',
167
+ 'submitLoginForm',
168
+ 'throwHttpError',
169
+ 'throwTokenEndpointError',
170
+ 'timestamp',
171
+ 'toSafeRedirect',
172
+ 'unflattenObject',
173
+ 'verifyIdToken',
174
+ 'verifyJwt',
175
+ 'createServerSDK',
176
+ 'createSessionIdCookieStorage',
177
+ 'COOKIE_CHUNK_SIZE',
178
+ 'COOKIE_CONTEXT',
179
+ 'createCacheAPIStorage',
180
+ 'createEncryptedCookieStorage',
181
+ 'createIndexedDBStorage',
182
+ 'createLocalStorage',
183
+ 'createMemoryStorage',
184
+ 'createServerMemoryStorage',
185
+ 'createServerStateStorage',
186
+ 'createSessionStorage',
187
+ 'createWorkerStorage',
188
+ 'handleWorkerStorageRequests',
189
+ 'AlgorithmTypeList',
190
+ 'GrantTypeList',
191
+ 'ResponseModeList',
192
+ 'ResponseTypeList',
193
+ 'SubjectTypeList',
194
+ 'TokenEndpointAuthMethodList',
195
+ ];
196
+
197
+ expect(Object.keys(serverIndex)).toEqual(expectedExports);
198
+ });
199
+ });