@nebulr-group/bridge-svelte 0.6.2 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/components/sdk-auth/LoginForm.svelte +30 -1
- package/dist/client/components/sdk-auth/SsoButton.svelte +13 -5
- package/dist/client/components/sdk-auth/SsoButton.svelte.d.ts +3 -1
- package/dist/client/components/sdk-auth/TenantSelector.svelte +9 -3
- package/dist/client/components/sdk-auth/TenantSelector.svelte.d.ts +3 -1
- package/dist/client/components/sdk-auth/WorkspaceSelector.svelte +9 -3
- package/dist/client/components/sdk-auth/WorkspaceSelector.svelte.d.ts +3 -1
- package/dist/client/components/sdk-auth/login-settling.test.d.ts +27 -0
- package/dist/client/components/sdk-auth/login-settling.test.js +219 -0
- package/dist/client/components/sdk-auth/selector-i18n.test.d.ts +1 -0
- package/dist/client/components/sdk-auth/selector-i18n.test.js +248 -0
- package/dist/styles.css +13 -0
- package/package.json +3 -3
|
@@ -237,7 +237,36 @@
|
|
|
237
237
|
<MfaSetup onError={onError} {messages} />
|
|
238
238
|
|
|
239
239
|
{:else if currentAuthState === 'tenant-selection'}
|
|
240
|
-
<TenantSelector onError={onError} />
|
|
240
|
+
<TenantSelector onError={onError} {messages} />
|
|
241
|
+
|
|
242
|
+
<!--
|
|
243
|
+
Settling: the session is real and the host app has not navigated yet.
|
|
244
|
+
|
|
245
|
+
This branch is `!== 'unauthenticated'` rather than an explicit list of
|
|
246
|
+
`authenticated | credentials-validated` on purpose. Those two used to fall
|
|
247
|
+
through to the credentials form below, so somebody who had just typed their
|
|
248
|
+
password correctly was shown the password form again — which reads as a
|
|
249
|
+
refusal, and the reasonable response is to type it again (TBP-635). The window
|
|
250
|
+
opens when the token exchange resolves and closes only when the consumer's
|
|
251
|
+
router lands, because LoginForm fires `onLogin` and deliberately does not
|
|
252
|
+
navigate. Measured at 600ms against a local stack; longer anywhere real.
|
|
253
|
+
|
|
254
|
+
Listing the two states would fix the two we know about and leave the next
|
|
255
|
+
`AuthState` member falling into the same hole. Inverting the test means the
|
|
256
|
+
credentials form renders ONLY for `unauthenticated`, and anything else lands
|
|
257
|
+
on a spinner — wrong-but-harmless instead of wrong-and-alarming.
|
|
258
|
+
|
|
259
|
+
`login.submitting` is reused rather than given its own key: it already says
|
|
260
|
+
"Signing in…" in all twelve locales, and a second key rendering the same words
|
|
261
|
+
would be a translation burden that buys nothing.
|
|
262
|
+
-->
|
|
263
|
+
{:else if currentAuthState !== 'unauthenticated'}
|
|
264
|
+
<AuthFormWrapper heading={null} class={className} {style} {...rest}>
|
|
265
|
+
<div class="bridge-auth-settling" data-bridge-auth-settling>
|
|
266
|
+
<Spinner size={24} />
|
|
267
|
+
<span>{t('login.submitting')}</span>
|
|
268
|
+
</div>
|
|
269
|
+
</AuthFormWrapper>
|
|
241
270
|
|
|
242
271
|
<!-- Inline forgot password -->
|
|
243
272
|
{:else if step === 'forgot-password'}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
4
|
-
import type { FederationConnection } from '@nebulr-group/bridge-auth-core';
|
|
4
|
+
import type { FederationConnection, MessageOverrides } from '@nebulr-group/bridge-auth-core';
|
|
5
5
|
import { getBridgeAuth } from '../../../core/bridge-instance.js';
|
|
6
|
+
import { getTranslator } from '../../stores/i18n.js';
|
|
6
7
|
import Spinner from './shared/Spinner.svelte';
|
|
7
8
|
|
|
8
9
|
interface Props extends HTMLButtonAttributes {
|
|
@@ -19,6 +20,8 @@
|
|
|
19
20
|
onSuccess?: () => void;
|
|
20
21
|
onError?: (error: Error) => void;
|
|
21
22
|
icon?: Snippet;
|
|
23
|
+
/** Per-key copy overrides for this component only (TBP-630). */
|
|
24
|
+
messages?: MessageOverrides;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
let {
|
|
@@ -28,13 +31,18 @@
|
|
|
28
31
|
onSuccess,
|
|
29
32
|
onError,
|
|
30
33
|
icon,
|
|
34
|
+
messages,
|
|
31
35
|
class: className,
|
|
32
36
|
style,
|
|
33
37
|
...rest
|
|
34
38
|
}: Props = $props();
|
|
35
39
|
|
|
40
|
+
const t = $derived(getTranslator(messages));
|
|
41
|
+
|
|
36
42
|
let loading = $state(false);
|
|
37
|
-
|
|
43
|
+
// `label` still wins: an app naming its own provider button is voice, not
|
|
44
|
+
// mechanics, and the catalogue only supplies the default (TBP-634).
|
|
45
|
+
let buttonLabel = $derived(label ?? t('sso.continueWith', { provider: connection.name }));
|
|
38
46
|
|
|
39
47
|
async function handleClick() {
|
|
40
48
|
if (loading) return;
|
|
@@ -44,13 +52,13 @@
|
|
|
44
52
|
if (result.type === 'auth_success') {
|
|
45
53
|
onSuccess?.();
|
|
46
54
|
} else if (result.type === 'auth_error') {
|
|
47
|
-
throw new Error(result.error || '
|
|
55
|
+
throw new Error(result.error || t('sso.error.login'));
|
|
48
56
|
}
|
|
49
57
|
// auth_mfa_required and auth_tenant_selection are handled by authState store
|
|
50
58
|
} catch (err: any) {
|
|
51
59
|
const message = err.message?.includes('popup')
|
|
52
|
-
? '
|
|
53
|
-
: err.message || '
|
|
60
|
+
? t('sso.error.popupBlocked')
|
|
61
|
+
: err.message || t('sso.error.login');
|
|
54
62
|
onError?.(new Error(message));
|
|
55
63
|
} finally {
|
|
56
64
|
loading = false;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
3
|
-
import type { FederationConnection } from '@nebulr-group/bridge-auth-core';
|
|
3
|
+
import type { FederationConnection, MessageOverrides } from '@nebulr-group/bridge-auth-core';
|
|
4
4
|
interface Props extends HTMLButtonAttributes {
|
|
5
5
|
connection: FederationConnection;
|
|
6
6
|
label?: string;
|
|
@@ -15,6 +15,8 @@ interface Props extends HTMLButtonAttributes {
|
|
|
15
15
|
onSuccess?: () => void;
|
|
16
16
|
onError?: (error: Error) => void;
|
|
17
17
|
icon?: Snippet;
|
|
18
|
+
/** Per-key copy overrides for this component only (TBP-630). */
|
|
19
|
+
messages?: MessageOverrides;
|
|
18
20
|
}
|
|
19
21
|
declare const SsoButton: import("svelte").Component<Props, {}, "">;
|
|
20
22
|
type SsoButton = ReturnType<typeof SsoButton>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
-
import type { TenantUser } from '@nebulr-group/bridge-auth-core';
|
|
4
|
+
import type { MessageOverrides, TenantUser } from '@nebulr-group/bridge-auth-core';
|
|
5
5
|
import { getBridgeAuth, tenantUsersStore } from '../../../core/bridge-instance.js';
|
|
6
|
+
import { getTranslator } from '../../stores/i18n.js';
|
|
6
7
|
import AuthFormWrapper from './shared/AuthFormWrapper.svelte';
|
|
7
8
|
import Spinner from './shared/Spinner.svelte';
|
|
8
9
|
import Alert from './shared/Alert.svelte';
|
|
@@ -11,17 +12,22 @@
|
|
|
11
12
|
onSelect?: () => void;
|
|
12
13
|
onError?: (error: Error) => void;
|
|
13
14
|
tenantItem?: Snippet<[TenantUser]>;
|
|
15
|
+
/** Per-key copy overrides for this component only (TBP-630). */
|
|
16
|
+
messages?: MessageOverrides;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
let {
|
|
17
20
|
onSelect,
|
|
18
21
|
onError,
|
|
19
22
|
tenantItem,
|
|
23
|
+
messages,
|
|
20
24
|
class: className,
|
|
21
25
|
style,
|
|
22
26
|
...rest
|
|
23
27
|
}: Props = $props();
|
|
24
28
|
|
|
29
|
+
const t = $derived(getTranslator(messages));
|
|
30
|
+
|
|
25
31
|
let loading = $state(false);
|
|
26
32
|
let selectedId = $state<string | null>(null);
|
|
27
33
|
let error = $state<string | null>(null);
|
|
@@ -35,7 +41,7 @@
|
|
|
35
41
|
await getBridgeAuth().selectTenant(tenantUser.id);
|
|
36
42
|
onSelect?.();
|
|
37
43
|
} catch (err: any) {
|
|
38
|
-
error = err.message || '
|
|
44
|
+
error = err.message || t('tenant.error.select');
|
|
39
45
|
onError?.(err);
|
|
40
46
|
} finally {
|
|
41
47
|
loading = false;
|
|
@@ -44,7 +50,7 @@
|
|
|
44
50
|
}
|
|
45
51
|
</script>
|
|
46
52
|
|
|
47
|
-
<AuthFormWrapper heading=
|
|
53
|
+
<AuthFormWrapper heading={t('tenant.chooseHeading')} class={className} {style} {...rest}>
|
|
48
54
|
{#if error}
|
|
49
55
|
<Alert variant="error">{error}</Alert>
|
|
50
56
|
{/if}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
-
import type { TenantUser } from '@nebulr-group/bridge-auth-core';
|
|
3
|
+
import type { MessageOverrides, TenantUser } from '@nebulr-group/bridge-auth-core';
|
|
4
4
|
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
5
5
|
onSelect?: () => void;
|
|
6
6
|
onError?: (error: Error) => void;
|
|
7
7
|
tenantItem?: Snippet<[TenantUser]>;
|
|
8
|
+
/** Per-key copy overrides for this component only (TBP-630). */
|
|
9
|
+
messages?: MessageOverrides;
|
|
8
10
|
}
|
|
9
11
|
declare const TenantSelector: import("svelte").Component<Props, {}, "">;
|
|
10
12
|
type TenantSelector = ReturnType<typeof TenantSelector>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
3
|
import type { Snippet } from 'svelte';
|
|
4
|
-
import type { Workspace } from '@nebulr-group/bridge-auth-core';
|
|
4
|
+
import type { MessageOverrides, Workspace } from '@nebulr-group/bridge-auth-core';
|
|
5
5
|
import { onMount } from 'svelte';
|
|
6
6
|
import { getBridgeAuth, profileStore } from '../../../core/bridge-instance.js';
|
|
7
|
+
import { getTranslator } from '../../stores/i18n.js';
|
|
7
8
|
import Spinner from './shared/Spinner.svelte';
|
|
8
9
|
import Alert from './shared/Alert.svelte';
|
|
9
10
|
|
|
@@ -11,17 +12,22 @@
|
|
|
11
12
|
onSwitch?: () => void;
|
|
12
13
|
onError?: (error: Error) => void;
|
|
13
14
|
workspaceItem?: Snippet<[{ workspace: Workspace; isActive: boolean; isLoading: boolean; onSelect: () => void }]>;
|
|
15
|
+
/** Per-key copy overrides for this component only (TBP-630). */
|
|
16
|
+
messages?: MessageOverrides;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
let {
|
|
17
20
|
onSwitch,
|
|
18
21
|
onError,
|
|
19
22
|
workspaceItem,
|
|
23
|
+
messages,
|
|
20
24
|
class: className,
|
|
21
25
|
style,
|
|
22
26
|
...rest
|
|
23
27
|
}: Props = $props();
|
|
24
28
|
|
|
29
|
+
const t = $derived(getTranslator(messages));
|
|
30
|
+
|
|
25
31
|
let workspaces = $state<Workspace[]>([]);
|
|
26
32
|
let loadError = $state<string | null>(null);
|
|
27
33
|
let loadingList = $state(true);
|
|
@@ -34,7 +40,7 @@
|
|
|
34
40
|
try {
|
|
35
41
|
workspaces = await getBridgeAuth().getWorkspaces();
|
|
36
42
|
} catch (err: any) {
|
|
37
|
-
loadError = err.message || '
|
|
43
|
+
loadError = err.message || t('workspace.error.load');
|
|
38
44
|
} finally {
|
|
39
45
|
loadingList = false;
|
|
40
46
|
}
|
|
@@ -48,7 +54,7 @@
|
|
|
48
54
|
await getBridgeAuth().switchWorkspace(workspace.id);
|
|
49
55
|
onSwitch?.();
|
|
50
56
|
} catch (err: any) {
|
|
51
|
-
switchError = err.message || '
|
|
57
|
+
switchError = err.message || t('workspace.error.switch');
|
|
52
58
|
onError?.(err);
|
|
53
59
|
} finally {
|
|
54
60
|
switchingId = null;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
-
import type { Workspace } from '@nebulr-group/bridge-auth-core';
|
|
3
|
+
import type { MessageOverrides, Workspace } from '@nebulr-group/bridge-auth-core';
|
|
4
4
|
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
5
5
|
onSwitch?: () => void;
|
|
6
6
|
onError?: (error: Error) => void;
|
|
@@ -10,6 +10,8 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
|
10
10
|
isLoading: boolean;
|
|
11
11
|
onSelect: () => void;
|
|
12
12
|
}]>;
|
|
13
|
+
/** Per-key copy overrides for this component only (TBP-630). */
|
|
14
|
+
messages?: MessageOverrides;
|
|
13
15
|
}
|
|
14
16
|
declare const WorkspaceSelector: import("svelte").Component<Props, {}, "">;
|
|
15
17
|
type WorkspaceSelector = ReturnType<typeof WorkspaceSelector>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TBP-635 — LoginForm must never draw the credentials form to somebody who has
|
|
3
|
+
* already authenticated.
|
|
4
|
+
*
|
|
5
|
+
* The top-level branch named `mfa-required`, `mfa-setup-required` and
|
|
6
|
+
* `tenant-selection`, and let everything else fall through to the password
|
|
7
|
+
* form. `authenticated` and `credentials-validated` are also "everything else",
|
|
8
|
+
* so between the token exchange resolving and the host app's router landing —
|
|
9
|
+
* LoginForm fires `onLogin` and deliberately does not navigate — the component
|
|
10
|
+
* showed a password field to a user who had just typed their password
|
|
11
|
+
* correctly. That reads as a refusal. Measured at 600ms against a local stack.
|
|
12
|
+
*
|
|
13
|
+
* ## What this file can and cannot prove
|
|
14
|
+
*
|
|
15
|
+
* The ticket's suggested test is a MutationObserver recording every DOM batch
|
|
16
|
+
* during a real login, because a sampling loop can miss the race. This
|
|
17
|
+
* workspace has no DOM (see auth-form-description.test.ts for why), so that
|
|
18
|
+
* exact test is not available here and belongs in the Playwright e2e suite.
|
|
19
|
+
*
|
|
20
|
+
* What IS available is stronger in the dimension that actually caused the bug.
|
|
21
|
+
* The defect was not a timing subtlety — it was an unhandled branch. So this
|
|
22
|
+
* enumerates EVERY member of `AuthState`, read out of auth-core's shipped
|
|
23
|
+
* type declaration rather than hardcoded, and asserts that exactly one of them
|
|
24
|
+
* renders a password field. A seventh state added to auth-core tomorrow is
|
|
25
|
+
* covered the moment it exists, which is the half a race-detector would miss.
|
|
26
|
+
*/
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TBP-635 — LoginForm must never draw the credentials form to somebody who has
|
|
3
|
+
* already authenticated.
|
|
4
|
+
*
|
|
5
|
+
* The top-level branch named `mfa-required`, `mfa-setup-required` and
|
|
6
|
+
* `tenant-selection`, and let everything else fall through to the password
|
|
7
|
+
* form. `authenticated` and `credentials-validated` are also "everything else",
|
|
8
|
+
* so between the token exchange resolving and the host app's router landing —
|
|
9
|
+
* LoginForm fires `onLogin` and deliberately does not navigate — the component
|
|
10
|
+
* showed a password field to a user who had just typed their password
|
|
11
|
+
* correctly. That reads as a refusal. Measured at 600ms against a local stack.
|
|
12
|
+
*
|
|
13
|
+
* ## What this file can and cannot prove
|
|
14
|
+
*
|
|
15
|
+
* The ticket's suggested test is a MutationObserver recording every DOM batch
|
|
16
|
+
* during a real login, because a sampling loop can miss the race. This
|
|
17
|
+
* workspace has no DOM (see auth-form-description.test.ts for why), so that
|
|
18
|
+
* exact test is not available here and belongs in the Playwright e2e suite.
|
|
19
|
+
*
|
|
20
|
+
* What IS available is stronger in the dimension that actually caused the bug.
|
|
21
|
+
* The defect was not a timing subtlety — it was an unhandled branch. So this
|
|
22
|
+
* enumerates EVERY member of `AuthState`, read out of auth-core's shipped
|
|
23
|
+
* type declaration rather than hardcoded, and asserts that exactly one of them
|
|
24
|
+
* renders a password field. A seventh state added to auth-core tomorrow is
|
|
25
|
+
* covered the moment it exists, which is the half a race-detector would miss.
|
|
26
|
+
*/
|
|
27
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
28
|
+
import { createServer } from 'vite';
|
|
29
|
+
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
30
|
+
import { render } from 'svelte/server';
|
|
31
|
+
import {} from 'svelte';
|
|
32
|
+
import { en, sv } from '@nebulr-group/bridge-auth-core';
|
|
33
|
+
import fs from 'node:fs';
|
|
34
|
+
import path from 'node:path';
|
|
35
|
+
const ROOT = process.cwd();
|
|
36
|
+
const COMPONENT_DIR = 'src/lib/client/components/sdk-auth';
|
|
37
|
+
const SEED_PREFIX = '__seeded-settling-';
|
|
38
|
+
/**
|
|
39
|
+
* Every `AuthState`, parsed out of the auth-core declaration the package
|
|
40
|
+
* actually ships.
|
|
41
|
+
*
|
|
42
|
+
* Deliberately not a literal list: a hardcoded one would still say six when
|
|
43
|
+
* auth-core says seven, and the bug being fixed is precisely a state nobody
|
|
44
|
+
* remembered to handle. Parsing throws rather than returning a short list, so a
|
|
45
|
+
* refactor that moves the type fails this suite loudly instead of quietly
|
|
46
|
+
* testing fewer states.
|
|
47
|
+
*/
|
|
48
|
+
function authStates() {
|
|
49
|
+
// Resolved from the filesystem rather than the module system: the package
|
|
50
|
+
// exports no `./package.json` subpath, and vitest's module runner supports
|
|
51
|
+
// neither `require.resolve` nor `import.meta.resolve` here. Both candidate
|
|
52
|
+
// paths are tried because bun may hoist the dep to the workspace root.
|
|
53
|
+
const candidates = [
|
|
54
|
+
path.join(ROOT, 'node_modules/@nebulr-group/bridge-auth-core/dist/types.d.ts'),
|
|
55
|
+
path.join(ROOT, '../node_modules/@nebulr-group/bridge-auth-core/dist/types.d.ts'),
|
|
56
|
+
];
|
|
57
|
+
const dts = candidates.find((c) => fs.existsSync(c));
|
|
58
|
+
if (!dts) {
|
|
59
|
+
throw new Error(`auth-core types.d.ts not found. Looked in:\n ${candidates.join('\n ')}`);
|
|
60
|
+
}
|
|
61
|
+
const source = fs.readFileSync(dts, 'utf8');
|
|
62
|
+
const match = source.match(/export type AuthState\s*=([^;]+);/);
|
|
63
|
+
if (!match) {
|
|
64
|
+
throw new Error('Could not find `export type AuthState` in auth-core types.d.ts.');
|
|
65
|
+
}
|
|
66
|
+
const states = [...match[1].matchAll(/'([^']+)'/g)].map((m) => m[1]);
|
|
67
|
+
if (states.length < 2) {
|
|
68
|
+
throw new Error(`Parsed only ${states.length} AuthState members — parser is wrong.`);
|
|
69
|
+
}
|
|
70
|
+
return states;
|
|
71
|
+
}
|
|
72
|
+
const STATES = authStates();
|
|
73
|
+
let server;
|
|
74
|
+
const seededPaths = [];
|
|
75
|
+
beforeAll(async () => {
|
|
76
|
+
for (const f of fs.readdirSync(path.join(ROOT, COMPONENT_DIR))) {
|
|
77
|
+
if (f.startsWith(SEED_PREFIX))
|
|
78
|
+
fs.unlinkSync(path.join(ROOT, COMPONENT_DIR, f));
|
|
79
|
+
}
|
|
80
|
+
server = await createServer({
|
|
81
|
+
configFile: false,
|
|
82
|
+
root: ROOT,
|
|
83
|
+
logLevel: 'error',
|
|
84
|
+
server: { middlewareMode: true, hmr: false },
|
|
85
|
+
plugins: [svelte({ compilerOptions: { dev: false } })],
|
|
86
|
+
});
|
|
87
|
+
}, 120_000);
|
|
88
|
+
afterAll(async () => {
|
|
89
|
+
await server?.close();
|
|
90
|
+
for (const p of seededPaths) {
|
|
91
|
+
if (fs.existsSync(p))
|
|
92
|
+
fs.unlinkSync(p);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
/**
|
|
96
|
+
* Render LoginForm with `currentAuthState` pinned.
|
|
97
|
+
*
|
|
98
|
+
* The seed replaces the one `$derived($authState)` line, so the template — every
|
|
99
|
+
* branch, the wrapper call, the catalogue lookups — is the real shipped source.
|
|
100
|
+
* Driving the store itself would need the network and the real state machine,
|
|
101
|
+
* and would test auth-core's transitions rather than this component's branching,
|
|
102
|
+
* which is where the defect was.
|
|
103
|
+
*/
|
|
104
|
+
async function renderAtState(state, props = {}) {
|
|
105
|
+
const file = 'LoginForm.svelte';
|
|
106
|
+
const from = 'let currentAuthState = $derived($authState);';
|
|
107
|
+
let source = fs.readFileSync(path.join(ROOT, COMPONENT_DIR, file), 'utf8');
|
|
108
|
+
if (!source.includes(from)) {
|
|
109
|
+
throw new Error(`State seed target not found in ${file}:\n ${from}\n` +
|
|
110
|
+
'The component was refactored. Update the seed so the test keeps driving ' +
|
|
111
|
+
'the auth-state branch — do not delete the assertion.');
|
|
112
|
+
}
|
|
113
|
+
source = source.replace(from, `let currentAuthState = $state('${state}');`);
|
|
114
|
+
const name = `${SEED_PREFIX}${process.pid}-${seededPaths.length}-${file}`;
|
|
115
|
+
const full = path.join(ROOT, COMPONENT_DIR, name);
|
|
116
|
+
fs.writeFileSync(full, source);
|
|
117
|
+
seededPaths.push(full);
|
|
118
|
+
const mod = await server.ssrLoadModule(`/${COMPONENT_DIR}/${name}`);
|
|
119
|
+
const Component = mod.default;
|
|
120
|
+
return render(Component, { props }).body;
|
|
121
|
+
}
|
|
122
|
+
async function setLocale(locale) {
|
|
123
|
+
const mod = await server.ssrLoadModule('/src/lib/client/stores/config.store.ts');
|
|
124
|
+
mod.bridgeConfig.initConfig({ appId: 'tbp-635-test', locale });
|
|
125
|
+
}
|
|
126
|
+
/** A password field is the thing that must never appear post-auth. */
|
|
127
|
+
function hasPasswordField(html) {
|
|
128
|
+
return /<input[^>]+type="password"/.test(html);
|
|
129
|
+
}
|
|
130
|
+
function text(html) {
|
|
131
|
+
return html
|
|
132
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
133
|
+
.replace(/<[^>]*>/g, ' ')
|
|
134
|
+
.replace(/\s+/g, ' ')
|
|
135
|
+
.trim();
|
|
136
|
+
}
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// 1. The fixture itself
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
describe('AuthState coverage (TBP-635)', () => {
|
|
141
|
+
it('reads the real state list out of the shipped auth-core types', () => {
|
|
142
|
+
// If this drifts, every assertion below silently covers less.
|
|
143
|
+
expect(STATES).toContain('unauthenticated');
|
|
144
|
+
expect(STATES).toContain('authenticated');
|
|
145
|
+
expect(STATES).toContain('credentials-validated');
|
|
146
|
+
expect(STATES.length).toBeGreaterThanOrEqual(6);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// 2. The invariant
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
describe('LoginForm never shows the credentials form post-auth (TBP-635)', () => {
|
|
153
|
+
it('renders a password field for `unauthenticated` and for nothing else', async () => {
|
|
154
|
+
await setLocale(undefined);
|
|
155
|
+
const withPassword = [];
|
|
156
|
+
for (const state of STATES) {
|
|
157
|
+
if (hasPasswordField(await renderAtState(state)))
|
|
158
|
+
withPassword.push(state);
|
|
159
|
+
}
|
|
160
|
+
// Both directions. Asserting only "authenticated has no password field"
|
|
161
|
+
// would pass for a component that rendered nothing at all, ever.
|
|
162
|
+
expect(withPassword).toEqual(['unauthenticated']);
|
|
163
|
+
});
|
|
164
|
+
for (const state of ['authenticated', 'credentials-validated']) {
|
|
165
|
+
it(`shows the settling card at "${state}"`, async () => {
|
|
166
|
+
await setLocale(undefined);
|
|
167
|
+
const html = await renderAtState(state);
|
|
168
|
+
expect(html).toContain('data-bridge-auth-settling');
|
|
169
|
+
expect(hasPasswordField(html)).toBe(false);
|
|
170
|
+
// Not a blank card: an empty box during a pause is its own bad message.
|
|
171
|
+
expect(text(html)).toContain(en['login.submitting']);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
it('translates the waiting copy', async () => {
|
|
175
|
+
await setLocale('sv');
|
|
176
|
+
const html = await renderAtState('authenticated');
|
|
177
|
+
expect(text(html)).toContain(sv['login.submitting']);
|
|
178
|
+
expect(text(html)).not.toContain(en['login.submitting']);
|
|
179
|
+
});
|
|
180
|
+
it('suppresses the heading while settling, so no stale "Log in" survives', async () => {
|
|
181
|
+
await setLocale(undefined);
|
|
182
|
+
const html = await renderAtState('authenticated', { heading: 'Log in to NorthWhistle' });
|
|
183
|
+
expect(html).not.toContain('Log in to NorthWhistle');
|
|
184
|
+
expect(html).toContain('data-bridge-auth-settling');
|
|
185
|
+
});
|
|
186
|
+
it('still hands the three delegated states to their own components', async () => {
|
|
187
|
+
// The fix must not have swallowed the branches that already worked — an
|
|
188
|
+
// over-eager `!== unauthenticated` placed above them would do exactly that.
|
|
189
|
+
await setLocale(undefined);
|
|
190
|
+
expect(await renderAtState('tenant-selection')).toContain('bridge-tenant-list');
|
|
191
|
+
expect(await renderAtState('mfa-required')).toContain(en['mfa.challengeHeading']);
|
|
192
|
+
expect(await renderAtState('mfa-setup-required')).toContain(en['mfaSetup.heading']);
|
|
193
|
+
});
|
|
194
|
+
it('leaves the unauthenticated step machine alone', async () => {
|
|
195
|
+
await setLocale(undefined);
|
|
196
|
+
const html = await renderAtState('unauthenticated');
|
|
197
|
+
expect(hasPasswordField(html)).toBe(true);
|
|
198
|
+
expect(html).toContain(en['login.submit']);
|
|
199
|
+
expect(html).not.toContain('data-bridge-auth-settling');
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// 3. onLogin is untouched
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
describe('onLogin lifecycle is unchanged (TBP-635)', () => {
|
|
206
|
+
it('still fires from an effect keyed on `authenticated`', () => {
|
|
207
|
+
// Effects do not run in a server render, so this is asserted on the source.
|
|
208
|
+
// It is here because the tempting wrong fix for this bug is to move
|
|
209
|
+
// `onLogin` earlier so the consumer navigates sooner — which would fire it
|
|
210
|
+
// before the session is real.
|
|
211
|
+
const source = fs.readFileSync(path.join(ROOT, COMPONENT_DIR, 'LoginForm.svelte'), 'utf8');
|
|
212
|
+
const effect = source.match(/\$effect\(\(\) => \{[\s\S]*?\}\);/);
|
|
213
|
+
expect(effect, 'LoginForm no longer has an $effect').not.toBeNull();
|
|
214
|
+
expect(effect[0]).toContain("currentAuthState === 'authenticated'");
|
|
215
|
+
expect(effect[0]).toContain('onLogin?.()');
|
|
216
|
+
// Exactly one call site, so it cannot fire twice.
|
|
217
|
+
expect(source.match(/onLogin\?\.\(\)/g)).toHaveLength(1);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// TenantSelector, WorkspaceSelector and SsoButton were missed by TBP-630's
|
|
2
|
+
// original i18n sweep and by the TBP-633 port, so they still rendered English
|
|
3
|
+
// in all four UI packages after everything around them had been translated.
|
|
4
|
+
// TenantSelector is the one that stings: it renders MID-LOGIN on the
|
|
5
|
+
// multi-workspace path, putting one English screen between a translated login
|
|
6
|
+
// form and a translated app (TBP-634).
|
|
7
|
+
//
|
|
8
|
+
// HARNESS NOTE — see auth-form-description.test.ts for the full explanation of
|
|
9
|
+
// why these render through Svelte's server renderer rather than a DOM. Short
|
|
10
|
+
// version: this workspace has no DOM implementation, so `mount()` and therefore
|
|
11
|
+
// clicks are unavailable. Two consequences shape this file.
|
|
12
|
+
//
|
|
13
|
+
// 1. Anything reachable at initial render (TenantSelector's heading,
|
|
14
|
+
// SsoButton's label) is asserted on the REAL rendered markup.
|
|
15
|
+
//
|
|
16
|
+
// 2. The five error strings are only produced inside a catch block a click
|
|
17
|
+
// would have to reach. Those get two assertions that are weak alone and
|
|
18
|
+
// strong together:
|
|
19
|
+
// - a `seeded()` render proving the KEY resolves to real Swedish, and
|
|
20
|
+
// - a source sweep proving the English literal the handler used to carry
|
|
21
|
+
// is gone from the file.
|
|
22
|
+
// Reverting the handler to `'Failed to select workspace.'` puts that exact
|
|
23
|
+
// string back in the source and fails the sweep. The sweep reads its
|
|
24
|
+
// forbidden strings out of auth-core's `en` catalogue, so it cannot drift
|
|
25
|
+
// from what the keys actually say.
|
|
26
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
27
|
+
import { createServer } from 'vite';
|
|
28
|
+
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
29
|
+
import { render } from 'svelte/server';
|
|
30
|
+
import {} from 'svelte';
|
|
31
|
+
import { en, sv } from '@nebulr-group/bridge-auth-core';
|
|
32
|
+
import fs from 'node:fs';
|
|
33
|
+
import path from 'node:path';
|
|
34
|
+
const ROOT = process.cwd();
|
|
35
|
+
const COMPONENT_DIR = 'src/lib/client/components/sdk-auth';
|
|
36
|
+
const SEED_PREFIX = '__seeded-i18n-';
|
|
37
|
+
/** The keys TBP-634 added, and which component owns each. */
|
|
38
|
+
const OWNED = {
|
|
39
|
+
'TenantSelector.svelte': ['tenant.chooseHeading', 'tenant.error.select'],
|
|
40
|
+
'WorkspaceSelector.svelte': ['workspace.error.load', 'workspace.error.switch'],
|
|
41
|
+
'SsoButton.svelte': ['sso.continueWith', 'sso.error.popupBlocked', 'sso.error.login'],
|
|
42
|
+
};
|
|
43
|
+
let server;
|
|
44
|
+
const seededPaths = [];
|
|
45
|
+
beforeAll(async () => {
|
|
46
|
+
for (const f of fs.readdirSync(path.join(ROOT, COMPONENT_DIR))) {
|
|
47
|
+
if (f.startsWith(SEED_PREFIX))
|
|
48
|
+
fs.unlinkSync(path.join(ROOT, COMPONENT_DIR, f));
|
|
49
|
+
}
|
|
50
|
+
server = await createServer({
|
|
51
|
+
configFile: false,
|
|
52
|
+
root: ROOT,
|
|
53
|
+
logLevel: 'error',
|
|
54
|
+
server: { middlewareMode: true, hmr: false },
|
|
55
|
+
plugins: [svelte({ compilerOptions: { dev: false } })],
|
|
56
|
+
});
|
|
57
|
+
}, 120_000);
|
|
58
|
+
afterAll(async () => {
|
|
59
|
+
await server?.close();
|
|
60
|
+
for (const p of seededPaths) {
|
|
61
|
+
if (fs.existsSync(p))
|
|
62
|
+
fs.unlinkSync(p);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
function componentId(file) {
|
|
66
|
+
return `/${COMPONENT_DIR}/${file}`;
|
|
67
|
+
}
|
|
68
|
+
/** A copy of `file` with initial-state literals replaced, to reach a later view. */
|
|
69
|
+
function seeded(file, seeds) {
|
|
70
|
+
let source = fs.readFileSync(path.join(ROOT, COMPONENT_DIR, file), 'utf8');
|
|
71
|
+
for (const [from, to] of seeds) {
|
|
72
|
+
if (!source.includes(from)) {
|
|
73
|
+
throw new Error(`State seed target not found in ${file}:\n ${from}\n` +
|
|
74
|
+
'The component was refactored. Update the seed so the test keeps ' +
|
|
75
|
+
'rendering the intended view — do not delete the assertion.');
|
|
76
|
+
}
|
|
77
|
+
source = source.replace(from, to);
|
|
78
|
+
}
|
|
79
|
+
const name = `${SEED_PREFIX}${process.pid}-${seededPaths.length}-${file}`;
|
|
80
|
+
const full = path.join(ROOT, COMPONENT_DIR, name);
|
|
81
|
+
fs.writeFileSync(full, source);
|
|
82
|
+
seededPaths.push(full);
|
|
83
|
+
return `/${COMPONENT_DIR}/${name}`;
|
|
84
|
+
}
|
|
85
|
+
async function renderComponent(id, props = {}) {
|
|
86
|
+
const mod = await server.ssrLoadModule(id);
|
|
87
|
+
const Component = mod.default;
|
|
88
|
+
return render(Component, { props }).body;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Point the shared config store at a locale.
|
|
92
|
+
*
|
|
93
|
+
* The components read locale off BridgeConfig rather than taking it as a prop,
|
|
94
|
+
* so this is how a real app sets it. Loaded through the same Vite module graph
|
|
95
|
+
* the components resolve, otherwise they would read a different store instance.
|
|
96
|
+
*/
|
|
97
|
+
async function setLocale(locale) {
|
|
98
|
+
const mod = await server.ssrLoadModule('/src/lib/client/stores/config.store.ts');
|
|
99
|
+
mod.bridgeConfig.initConfig({ appId: 'tbp-634-test', locale });
|
|
100
|
+
}
|
|
101
|
+
const CONNECTION = { type: 'google', name: 'Google' };
|
|
102
|
+
function text(html) {
|
|
103
|
+
return html
|
|
104
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
105
|
+
.replace(/<[^>]*>/g, '')
|
|
106
|
+
.replace(/\s+/g, ' ')
|
|
107
|
+
.trim();
|
|
108
|
+
}
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// 1. What the server renderer can reach directly
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
describe('TenantSelector heading (TBP-634)', () => {
|
|
113
|
+
it('renders the English heading when no locale is set', async () => {
|
|
114
|
+
await setLocale(undefined);
|
|
115
|
+
const html = await renderComponent(componentId('TenantSelector.svelte'));
|
|
116
|
+
expect(text(html)).toContain(en['tenant.chooseHeading']);
|
|
117
|
+
});
|
|
118
|
+
it('renders the Swedish heading at locale sv', async () => {
|
|
119
|
+
await setLocale('sv');
|
|
120
|
+
const html = await renderComponent(componentId('TenantSelector.svelte'));
|
|
121
|
+
const rendered = text(html);
|
|
122
|
+
expect(rendered).toContain(sv['tenant.chooseHeading']);
|
|
123
|
+
// The English must be GONE, not merely joined by the Swedish. A component
|
|
124
|
+
// that rendered both would satisfy a `toContain` on the Swedish alone.
|
|
125
|
+
expect(rendered).not.toContain(en['tenant.chooseHeading']);
|
|
126
|
+
});
|
|
127
|
+
it('lets a per-component override beat the locale', async () => {
|
|
128
|
+
await setLocale('sv');
|
|
129
|
+
const html = await renderComponent(componentId('TenantSelector.svelte'), {
|
|
130
|
+
messages: { 'tenant.chooseHeading': 'Välj kund' },
|
|
131
|
+
});
|
|
132
|
+
const rendered = text(html);
|
|
133
|
+
expect(rendered).toContain('Välj kund');
|
|
134
|
+
expect(rendered).not.toContain(sv['tenant.chooseHeading']);
|
|
135
|
+
});
|
|
136
|
+
it('never renders the raw key', async () => {
|
|
137
|
+
for (const locale of ['sv', 'de', 'klingon', undefined]) {
|
|
138
|
+
await setLocale(locale);
|
|
139
|
+
const html = await renderComponent(componentId('TenantSelector.svelte'));
|
|
140
|
+
expect(text(html)).not.toContain('tenant.chooseHeading');
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
describe('SsoButton label (TBP-634)', () => {
|
|
145
|
+
it('interpolates the provider name into the localised label', async () => {
|
|
146
|
+
await setLocale('sv');
|
|
147
|
+
const html = await renderComponent(componentId('SsoButton.svelte'), {
|
|
148
|
+
connection: CONNECTION,
|
|
149
|
+
});
|
|
150
|
+
const rendered = text(html);
|
|
151
|
+
expect(rendered).toBe(sv['sso.continueWith'].replace('{provider}', 'Google'));
|
|
152
|
+
// Placeholder consumed, not printed.
|
|
153
|
+
expect(rendered).not.toContain('{provider}');
|
|
154
|
+
expect(rendered).not.toContain(en['sso.continueWith'].replace('{provider}', 'Google'));
|
|
155
|
+
});
|
|
156
|
+
it('still lets the app supply its own label — that is voice, not mechanics', async () => {
|
|
157
|
+
await setLocale('sv');
|
|
158
|
+
const html = await renderComponent(componentId('SsoButton.svelte'), {
|
|
159
|
+
connection: CONNECTION,
|
|
160
|
+
label: 'Logga in med jobbkontot',
|
|
161
|
+
});
|
|
162
|
+
expect(text(html)).toBe('Logga in med jobbkontot');
|
|
163
|
+
});
|
|
164
|
+
it('falls back to English for an unsupported locale, never to the key', async () => {
|
|
165
|
+
// Invented, not a real-but-unshipped code: TBP-632 turned 'de' and 'fr'
|
|
166
|
+
// into shipped locales and broke every test that had borrowed one as its
|
|
167
|
+
// stand-in for 'unknown'.
|
|
168
|
+
await setLocale('klingon');
|
|
169
|
+
const html = await renderComponent(componentId('SsoButton.svelte'), {
|
|
170
|
+
connection: CONNECTION,
|
|
171
|
+
});
|
|
172
|
+
expect(text(html)).toBe(en['sso.continueWith'].replace('{provider}', 'Google'));
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// 2. The error strings, reached by seeding initial state
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
describe('selector error copy resolves through the catalogue (TBP-634)', () => {
|
|
179
|
+
const CASES = [
|
|
180
|
+
{
|
|
181
|
+
file: 'TenantSelector.svelte',
|
|
182
|
+
from: 'let error = $state<string | null>(null);',
|
|
183
|
+
key: 'tenant.error.select',
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
file: 'WorkspaceSelector.svelte',
|
|
187
|
+
from: 'let loadError = $state<string | null>(null);',
|
|
188
|
+
key: 'workspace.error.load',
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
file: 'WorkspaceSelector.svelte',
|
|
192
|
+
from: 'let switchError = $state<string | null>(null);',
|
|
193
|
+
key: 'workspace.error.switch',
|
|
194
|
+
},
|
|
195
|
+
];
|
|
196
|
+
for (const { file, from, key } of CASES) {
|
|
197
|
+
it(`${file} renders ${key} in Swedish`, async () => {
|
|
198
|
+
await setLocale('sv');
|
|
199
|
+
const to = from.replace('(null)', `(t('${key}'))`);
|
|
200
|
+
const html = await renderComponent(seeded(file, [[from, to]]));
|
|
201
|
+
const rendered = text(html);
|
|
202
|
+
expect(rendered).toContain(sv[key]);
|
|
203
|
+
expect(rendered).not.toContain(en[key]);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// 3. Source sweep — the half that survives a reverted handler
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
describe('no English literal remains in the three components (TBP-634)', () => {
|
|
211
|
+
for (const [file, keys] of Object.entries(OWNED)) {
|
|
212
|
+
it(`${file} carries none of its English strings as a literal`, () => {
|
|
213
|
+
const source = fs.readFileSync(path.join(ROOT, COMPONENT_DIR, file), 'utf8');
|
|
214
|
+
for (const key of keys) {
|
|
215
|
+
// Interpolated copy is compared on its literal prefix: the source used
|
|
216
|
+
// to hold `Continue with ${connection.name}`, which is the catalogue
|
|
217
|
+
// string with the placeholder swapped for an expression.
|
|
218
|
+
const needle = en[key].split('{')[0].trim();
|
|
219
|
+
expect(needle.length, `${key} has no comparable literal prefix`).toBeGreaterThan(4);
|
|
220
|
+
expect(source, `${file} still contains "${needle}"`).not.toContain(needle);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
it('the sweep is looking at real files with real content', () => {
|
|
225
|
+
// Guard against the whole suite passing because a rename made every
|
|
226
|
+
// readFileSync return something empty or unrelated.
|
|
227
|
+
for (const file of Object.keys(OWNED)) {
|
|
228
|
+
const source = fs.readFileSync(path.join(ROOT, COMPONENT_DIR, file), 'utf8');
|
|
229
|
+
expect(source.length).toBeGreaterThan(500);
|
|
230
|
+
expect(source).toContain('getTranslator');
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// 4. LoginForm fans `messages` down to TenantSelector
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
describe('LoginForm → TenantSelector fan-out (TBP-634)', () => {
|
|
238
|
+
it('passes messages down, the way it already does to MfaChallenge', () => {
|
|
239
|
+
// Only reachable on the multi-workspace path, which needs auth state a
|
|
240
|
+
// server render cannot produce — so this is asserted on the template. It is
|
|
241
|
+
// the easiest of the fan-outs to forget precisely because it so rarely
|
|
242
|
+
// renders.
|
|
243
|
+
const source = fs.readFileSync(path.join(ROOT, COMPONENT_DIR, 'LoginForm.svelte'), 'utf8');
|
|
244
|
+
const tag = source.match(/<TenantSelector[^>]*\/>/);
|
|
245
|
+
expect(tag, 'LoginForm no longer renders <TenantSelector>').not.toBeNull();
|
|
246
|
+
expect(tag[0]).toContain('{messages}');
|
|
247
|
+
});
|
|
248
|
+
});
|
package/dist/styles.css
CHANGED
|
@@ -309,6 +309,19 @@
|
|
|
309
309
|
margin: 0 0 1rem;
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
+
/* TBP-635 — the card between a successful sign-in and the host app navigating. */
|
|
313
|
+
.bridge-auth-settling {
|
|
314
|
+
display: flex;
|
|
315
|
+
align-items: center;
|
|
316
|
+
justify-content: center;
|
|
317
|
+
gap: 0.75rem;
|
|
318
|
+
/* Roughly the height of the credentials form it replaces, so the card does
|
|
319
|
+
not collapse and then re-expand under the cursor. */
|
|
320
|
+
min-height: 8rem;
|
|
321
|
+
color: var(--bridge-muted, #6b7280);
|
|
322
|
+
font-size: 0.875rem;
|
|
323
|
+
}
|
|
324
|
+
|
|
312
325
|
.bridge-mfa-help {
|
|
313
326
|
font-size: 0.8125rem;
|
|
314
327
|
color: var(--bridge-muted, #6b7280);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nebulr-group/bridge-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Bridge Svelte library, This library helps you to add bridge authentication and feature flags, and payments to your svelte application.",
|
|
5
5
|
"author": "Iman Pouya",
|
|
6
6
|
"license": "MIT",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"svelte": "^5.0.0",
|
|
57
57
|
"@sveltejs/kit": "^2.0.0",
|
|
58
58
|
"@stripe/stripe-js": ">=4.0.0",
|
|
59
|
-
"@nebulr-group/bridge-auth-core": ">=0.
|
|
59
|
+
"@nebulr-group/bridge-auth-core": ">=0.6.0 <1.0.0"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
62
62
|
"@stripe/stripe-js": {
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"typescript": "^6.0.0",
|
|
78
78
|
"vite": "^6.2.6",
|
|
79
79
|
"vitest": "^4.1.4",
|
|
80
|
-
"@nebulr-group/bridge-auth-core": "^0.
|
|
80
|
+
"@nebulr-group/bridge-auth-core": "^0.6.0"
|
|
81
81
|
},
|
|
82
82
|
"keywords": [
|
|
83
83
|
"svelte",
|