@crowdedkingdoms/crowdyjs 14.2.0-dev.1 → 15.0.0-dev.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/MIGRATION.md CHANGED
@@ -1,3 +1,55 @@
1
+ # CrowdyJS v15 — the dev auth bypass is gone (breaking)
2
+
3
+ `client.auth.devLogin()` is **removed**, and so is the `devToken` field on
4
+ `requestLoginLink`. Neither is deprecated or disabled — the server-side feature
5
+ they called is deleted from every tier, so a wrapper for it could only produce a
6
+ GraphQL validation error.
7
+
8
+ **Why it went.** `devLogin` returned an identity session for any email address
9
+ with no proof of ownership whatsoever. It was gated on a server flag the control
10
+ plane derived as `tier !== 'prod'`, so it was live on dev and test, and if the
11
+ address happened to belong to a super admin then so did the session. `devToken`
12
+ was the same hole in a smaller shape: it put the emailed one-time magic-link
13
+ token in the response body, readable by any unauthenticated caller who knew an
14
+ address.
15
+
16
+ **What replaces them: `login` and `register`, which are new here and are not new
17
+ to the server.** Email + password has been first-class in the API throughout;
18
+ only this SDK claimed the product was passwordless, and that gap is what pushed
19
+ automated clients onto the bypass in the first place.
20
+
21
+ ```diff
22
+ -await client.auth.devLogin('player@example.com');
23
+ +await client.auth.login({ email: 'player@example.com', password });
24
+ +// or, for an address that has never been seen:
25
+ +await client.auth.register({ email: 'player@example.com', password });
26
+ ```
27
+
28
+ ```diff
29
+ const link = await client.auth.requestLoginLink({ email });
30
+ -if (link.devToken) await client.auth.completeLoginLink(link.devToken);
31
+ +// The token arrives only by email now. An automated caller should register an
32
+ +// account it holds the password to instead of reading one out of the response.
33
+ ```
34
+
35
+ **Also new:** `client.auth.checkAuthMethod(email)` for email-first adaptive
36
+ login, and two error predicates, because these two conditions are **not**
37
+ distinguishable by GraphQL error code:
38
+
39
+ - `isAlreadyRegisteredError(e)` — `register` refused because the address already
40
+ has an account. The server raises a `ConflictException` and it arrives as
41
+ `INTERNAL_SERVER_ERROR`, so a caller keying on `CONFLICT` matches nothing.
42
+ - `isPasswordUnconfirmedError(e)` — `login` refused because the password is real
43
+ but unconfirmed on an account with another verified sign-in method. The remedy
44
+ is the emailed link, not a different password.
45
+
46
+ **One behaviour worth knowing before you write a retry loop:** `register` returns
47
+ a session only for an address it is **creating**. An address that already has an
48
+ account gets the password attached *pending email confirmation* and no token.
49
+ Registering and signing in are therefore not interchangeable.
50
+
51
+ ---
52
+
1
53
  # CrowdyJS v14 — one endpoint (breaking)
2
54
 
3
55
  v13 made the two GraphQL origins optional-but-supported. v14 removes the second
@@ -489,11 +541,12 @@ your sign-in flow to one of:
489
541
  await client.auth.socialLoginComplete({ provider: 'google', code, state });
490
542
  ```
491
543
  - **Dev bypass (development only):** `await client.auth.devLogin(email)` — works only
492
- when the server has `DEV_AUTH_BYPASS` enabled.
544
+ when the server has `DEV_AUTH_BYPASS` enabled. **Removed in 15.0.0 — see below.**
493
545
 
494
546
  **Removed:** `client.auth.login`, `register`, `confirmEmail`, `requestPasswordReset`,
495
547
  `resetPassword`, `resendConfirmationEmail`, `changePassword` (and the
496
548
  `LoginUserInput` / `RegisterUserInput` / `ResetPasswordInput` types).
549
+ **`login` and `register` came back in 15.0.0**; the rest did not.
497
550
 
498
551
  **New:** `requestLoginLink`, `completeLoginLink`, `socialLoginStart`,
499
552
  `socialLoginComplete`, `devLogin`, `availableLoginProviders`, `myIdentities`,
package/README.md CHANGED
@@ -49,18 +49,20 @@ const client = createCrowdyClient({
49
49
  },
50
50
  });
51
51
 
52
- // Restore a previous session if there is one, otherwise sign in (passwordless).
52
+ // Restore a previous session if there is one, otherwise sign in.
53
53
  await client.session.restore();
54
54
  if (!client.session.getToken()) {
55
- // Magic link: email a one-time link, then complete with the token from it.
55
+ // Email + password:
56
+ await client.auth.login({ email: 'player@example.com', password });
57
+ // ...or create the account: client.auth.register({ email, password })
58
+ // Or magic link: email a one-time link, then complete with the token from it.
56
59
  await client.auth.requestLoginLink({ email: 'player@example.com', redirectUri });
57
60
  await client.auth.completeLoginLink(tokenFromLink);
58
61
  // Or social/OIDC: socialLoginStart('google', redirectUri) -> socialLoginComplete({ provider, code, state })
59
- // Or dev/test only (server has DEV_AUTH_BYPASS): client.auth.devLogin('player@example.com')
60
62
  }
61
63
 
62
- // Passwordless sign-in returns an identity SESSION token (rejected for gameplay);
63
- // the account is created on first sign-in. Identity reads run on it:
64
+ // Every sign-in returns an identity SESSION token (rejected for gameplay).
65
+ // Identity reads run on it:
64
66
  const me = await client.users.me();
65
67
  console.log(me.email);
66
68
  ```
@@ -725,8 +727,8 @@ endpoint; `client.graphql` reaches every surface.)
725
727
  the environment is not configured, so they are safe to leave in `npm test`.
726
728
 
727
729
  ```bash
728
- CROWDY_HTTP_URL='https://ck.<tier>.cp.cks-env.com' \
729
- CROWDY_WS_URL='wss://ck.<tier>.cp.cks-env.com/graphql' \
730
+ CROWDY_HTTP_URL='https://ck.<tier>.v7.cks-env.com' \
731
+ CROWDY_WS_URL='wss://ck.<tier>.v7.cks-env.com/graphql' \
730
732
  CROWDY_OWNER_EMAIL='owner@example.com' \
731
733
  CROWDY_TEST_APP_ID='78221653114368' \
732
734
  npm run test:e2e
@@ -3,17 +3,24 @@ import type { AuthState } from '../auth-state.js';
3
3
  /**
4
4
  * Authentication and account lifecycle — exposed as `client.auth`.
5
5
  *
6
- * Crowded Kingdoms is **passwordless**. There is no email+password login: a user
7
- * authenticates with an emailed magic link, a federated social provider (OIDC),
8
- * or in development only the dev bypass. Every path returns an identity
9
- * SESSION token (management-plane), which is stored on the shared session state
10
- * automatically. Gameplay tokens are minted separately via `client.portal`.
6
+ * Four ways to sign in: email + password ({@link register} / {@link login}), an
7
+ * emailed magic link, or a federated social provider (OIDC). Every path returns
8
+ * an identity SESSION token (management-plane), which is stored on the shared
9
+ * session state automatically. Gameplay tokens are minted separately via
10
+ * `client.portal`.
11
+ *
12
+ * This comment used to say Crowded Kingdoms was passwordless and that there was
13
+ * no email+password login. That was never true of the server: `login` and
14
+ * `register` have been first-class in the API throughout, and the C++ load
15
+ * tester has used them all along. Only this SDK pretended otherwise, and the
16
+ * gap sent integrators to the dev bypass — which no longer exists on any tier.
11
17
  *
12
18
  * Part of the management surface.
13
19
  *
14
- * **Public (no session):** {@link requestLoginLink}, {@link completeLoginLink},
15
- * {@link socialLoginStart}, {@link socialLoginComplete}, {@link devLogin},
16
- * {@link availableLoginProviders}. **Require a session:** {@link logout},
20
+ * **Public (no session):** {@link register}, {@link login},
21
+ * {@link requestLoginLink}, {@link completeLoginLink}, {@link socialLoginStart},
22
+ * {@link socialLoginComplete}, {@link availableLoginProviders},
23
+ * {@link checkAuthMethod}. **Require a session:** {@link logout},
17
24
  * {@link logoutAllDevices}, {@link myIdentities}, {@link linkIdentity},
18
25
  * {@link unlinkIdentity}.
19
26
  */
@@ -37,6 +44,23 @@ export interface UserIdentity {
37
44
  createdAt: string;
38
45
  lastLoginAt: string | null;
39
46
  }
47
+ /**
48
+ * `register` refused because the address already has an account.
49
+ *
50
+ * Matched on WORDING rather than on an error code, which looks fragile and is
51
+ * the only thing that works: the server raises a Nest `ConflictException` and it
52
+ * arrives over GraphQL as `INTERNAL_SERVER_ERROR` — verified against a live
53
+ * tier — so a caller keying on `CONFLICT` matches nothing and treats a routine
54
+ * "this account exists" as a server fault.
55
+ */
56
+ export declare function isAlreadyRegisteredError(error: unknown): boolean;
57
+ /**
58
+ * `login` refused because the password is real but not yet confirmed, on an
59
+ * account that has another verified sign-in method. The remedy is the emailed
60
+ * confirmation link, not a different password — so this must not be reported to
61
+ * the user as "wrong password".
62
+ */
63
+ export declare function isPasswordUnconfirmedError(error: unknown): boolean;
40
64
  export declare class AuthAPI {
41
65
  private readonly graphql;
42
66
  private readonly session;
@@ -45,16 +69,17 @@ export declare class AuthAPI {
45
69
  availableLoginProviders(): Promise<string[]>;
46
70
  /**
47
71
  * Passwordless: email the address a one-time magic sign-in link (creating the
48
- * account on first sign-in). Always resolves `sent: true` (no enumeration). In
49
- * development (`DEV_AUTH_BYPASS`) the response also carries `devToken`, the
50
- * token to pass straight to {@link completeLoginLink} without an inbox.
72
+ * account on first sign-in). Always resolves `sent: true` (no enumeration).
73
+ *
74
+ * The token arrives only by email. There is no longer a `devToken` shortcut —
75
+ * automated callers that need a session without an inbox should
76
+ * {@link register} an account they own the password to.
51
77
  */
52
78
  requestLoginLink(input: {
53
79
  email: string;
54
80
  redirectUri?: string;
55
81
  }): Promise<{
56
82
  sent: boolean;
57
- devToken: string | null;
58
83
  }>;
59
84
  /** Complete a magic-link sign-in; stores the session token on success. */
60
85
  completeLoginLink(token: string): Promise<AuthResponse>;
@@ -73,11 +98,39 @@ export declare class AuthAPI {
73
98
  state: string;
74
99
  }): Promise<AuthResponse>;
75
100
  /**
76
- * DEV ONLY bypass sign-in (active only when the server has `DEV_AUTH_BYPASS`).
77
- * Returns a session for `email` without email/social verification; stores it.
78
- * Throws `FORBIDDEN` when the bypass is disabled (e.g. production).
101
+ * Sign in with email + password; stores the session token on success.
102
+ *
103
+ * Throws when the credentials are wrong, and — separately — when the account
104
+ * has another verified sign-in method and the password has not yet been
105
+ * confirmed by email. {@link isPasswordUnconfirmedError} tells those apart,
106
+ * because they need different things from the user.
79
107
  */
80
- devLogin(email: string): Promise<AuthResponse>;
108
+ login(input: {
109
+ email: string;
110
+ password: string;
111
+ }): Promise<AuthResponse>;
112
+ /**
113
+ * Create an email + password account; stores the session token on success.
114
+ *
115
+ * **A brand-new address gets a session immediately.** An address that already
116
+ * has an account does NOT: the password is attached pending email
117
+ * confirmation and the server throws instead of returning a token, so the
118
+ * caller cannot treat "registered" and "signed in" as one outcome. Use
119
+ * {@link isAlreadyRegisteredError} to detect it and fall back to
120
+ * {@link login} or {@link requestLoginLink}.
121
+ */
122
+ register(input: {
123
+ email: string;
124
+ password: string;
125
+ gamertag?: string;
126
+ }): Promise<AuthResponse>;
127
+ /**
128
+ * Email-first adaptive login: does this address have password sign-in enabled?
129
+ * Public, and deliberately does not reveal whether the address is registered.
130
+ */
131
+ checkAuthMethod(email: string): Promise<{
132
+ hasPassword: boolean;
133
+ }>;
81
134
  /** The signed-in user's linked sign-in identities. Requires a session. */
82
135
  myIdentities(): Promise<UserIdentity[]>;
83
136
  /** Link an additional federated identity (from a social callback). */
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/domains/auth.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGlD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AA+DD,qBAAa,OAAO;IAEhB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBADP,OAAO,EAAE,aAAa,EACtB,OAAO,EAAE,SAAS;IAGrC,6EAA6E;IACvE,uBAAuB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAKlD;;;;;OAKG;IACG,gBAAgB,CAAC,KAAK,EAAE;QAC5B,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAKvD,0EAA0E;IACpE,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAS7D;;;OAGG;IACG,gBAAgB,CACpB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAOnD,iFAAiF;IAC3E,mBAAmB,CAAC,KAAK,EAAE;QAC/B,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;KACf,GAAG,OAAO,CAAC,YAAY,CAAC;IASzB;;;;OAIG;IACG,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAQpD,0EAA0E;IACpE,YAAY,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAK7C,sEAAsE;IAChE,YAAY,CAAC,KAAK,EAAE;QACxB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;KACf,GAAG,OAAO,CAAC,YAAY,CAAC;IAKzB,2EAA2E;IACrE,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAO1D,mEAAmE;IAC7D,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC;IAMhC,oEAAoE;IAC9D,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC;IAK1C,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAIpC,+CAA+C;IAC/C,QAAQ,IAAI,MAAM,GAAG,IAAI;CAG1B"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/domains/auth.ts"],"names":[],"mappings":"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"}
@@ -1,12 +1,46 @@
1
1
  import { parse } from 'graphql';
2
2
  import { LogoutAllDevicesDocument, LogoutDocument } from '../generated/graphql.js';
3
+ /**
4
+ * `register` refused because the address already has an account.
5
+ *
6
+ * Matched on WORDING rather than on an error code, which looks fragile and is
7
+ * the only thing that works: the server raises a Nest `ConflictException` and it
8
+ * arrives over GraphQL as `INTERNAL_SERVER_ERROR` — verified against a live
9
+ * tier — so a caller keying on `CONFLICT` matches nothing and treats a routine
10
+ * "this account exists" as a server fault.
11
+ */
12
+ export function isAlreadyRegisteredError(error) {
13
+ const message = error instanceof Error ? error.message : String(error);
14
+ return /account with this email already exists/i.test(message);
15
+ }
16
+ /**
17
+ * `login` refused because the password is real but not yet confirmed, on an
18
+ * account that has another verified sign-in method. The remedy is the emailed
19
+ * confirmation link, not a different password — so this must not be reported to
20
+ * the user as "wrong password".
21
+ */
22
+ export function isPasswordUnconfirmedError(error) {
23
+ const message = error instanceof Error ? error.message : String(error);
24
+ return /confirm your email to enable password sign-in/i.test(message);
25
+ }
3
26
  const AUTH_RESPONSE_FIELDS = 'token gameTokenId user { userId email gamertag }';
4
27
  const IDENTITY_FIELDS = 'identityId provider subject email emailVerified createdAt lastLoginAt';
5
- const RequestLoginLinkDocument = parse(`mutation RequestLoginLink($input: RequestLoginLinkInput!) { requestLoginLink(input: $input) { sent devToken } }`);
28
+ // `devToken` is NOT selected. The field returned the magic-link token in
29
+ // plaintext whenever the server had the dev bypass on, which made an emailed
30
+ // one-time secret readable by any unauthenticated caller. It is gone from the
31
+ // server; selecting it here would make every requestLoginLink call fail
32
+ // validation against a current API.
33
+ const RequestLoginLinkDocument = parse(`mutation RequestLoginLink($input: RequestLoginLinkInput!) { requestLoginLink(input: $input) { sent } }`);
6
34
  const CompleteLoginLinkDocument = parse(`mutation CompleteLoginLink($input: CompleteLoginLinkInput!) { completeLoginLink(input: $input) { ${AUTH_RESPONSE_FIELDS} } }`);
7
35
  const SocialLoginStartDocument = parse(`mutation SocialLoginStart($input: SocialLoginStartInput!) { socialLoginStart(input: $input) { authorizeUrl state } }`);
8
36
  const SocialLoginCompleteDocument = parse(`mutation SocialLoginComplete($input: SocialLoginCompleteInput!) { socialLoginComplete(input: $input) { ${AUTH_RESPONSE_FIELDS} } }`);
9
- const DevLoginDocument = parse(`mutation DevLogin($input: DevLoginInput!) { devLogin(input: $input) { ${AUTH_RESPONSE_FIELDS} } }`);
37
+ // `login` and `register` take a NON-STANDARD argument name -- `loginUserInput`
38
+ // and `registerUserInput` rather than `input`. That is the server's spelling and
39
+ // it is not negotiable from here; getting it wrong produces a validation error
40
+ // naming a field the caller never wrote.
41
+ const LoginDocument = parse(`mutation Login($loginUserInput: LoginUserInput!) { login(loginUserInput: $loginUserInput) { ${AUTH_RESPONSE_FIELDS} } }`);
42
+ const RegisterDocument = parse(`mutation Register($registerUserInput: RegisterUserInput!) { register(registerUserInput: $registerUserInput) { ${AUTH_RESPONSE_FIELDS} } }`);
43
+ const CheckAuthMethodDocument = parse(`query CheckAuthMethod($input: CheckAuthMethodInput!) { checkAuthMethod(input: $input) { hasPassword } }`);
10
44
  const AvailableLoginProvidersDocument = parse(`query AvailableLoginProviders { availableLoginProviders }`);
11
45
  const MyIdentitiesDocument = parse(`query MyIdentities { myIdentities { ${IDENTITY_FIELDS} } }`);
12
46
  const LinkIdentityDocument = parse(`mutation LinkIdentity($input: LinkIdentityInput!) { linkIdentity(input: $input) { ${IDENTITY_FIELDS} } }`);
@@ -23,9 +57,11 @@ export class AuthAPI {
23
57
  }
24
58
  /**
25
59
  * Passwordless: email the address a one-time magic sign-in link (creating the
26
- * account on first sign-in). Always resolves `sent: true` (no enumeration). In
27
- * development (`DEV_AUTH_BYPASS`) the response also carries `devToken`, the
28
- * token to pass straight to {@link completeLoginLink} without an inbox.
60
+ * account on first sign-in). Always resolves `sent: true` (no enumeration).
61
+ *
62
+ * The token arrives only by email. There is no longer a `devToken` shortcut —
63
+ * automated callers that need a session without an inbox should
64
+ * {@link register} an account they own the password to.
29
65
  */
30
66
  async requestLoginLink(input) {
31
67
  const data = await this.graphql.request(RequestLoginLinkDocument, { input });
@@ -60,17 +96,48 @@ export class AuthAPI {
60
96
  return data.socialLoginComplete;
61
97
  }
62
98
  /**
63
- * DEV ONLY bypass sign-in (active only when the server has `DEV_AUTH_BYPASS`).
64
- * Returns a session for `email` without email/social verification; stores it.
65
- * Throws `FORBIDDEN` when the bypass is disabled (e.g. production).
99
+ * Sign in with email + password; stores the session token on success.
100
+ *
101
+ * Throws when the credentials are wrong, and — separately — when the account
102
+ * has another verified sign-in method and the password has not yet been
103
+ * confirmed by email. {@link isPasswordUnconfirmedError} tells those apart,
104
+ * because they need different things from the user.
105
+ */
106
+ async login(input) {
107
+ const data = await this.graphql.request(LoginDocument, {
108
+ loginUserInput: input,
109
+ });
110
+ if (data.login?.token)
111
+ this.session.setToken(data.login.token);
112
+ return data.login;
113
+ }
114
+ /**
115
+ * Create an email + password account; stores the session token on success.
116
+ *
117
+ * **A brand-new address gets a session immediately.** An address that already
118
+ * has an account does NOT: the password is attached pending email
119
+ * confirmation and the server throws instead of returning a token, so the
120
+ * caller cannot treat "registered" and "signed in" as one outcome. Use
121
+ * {@link isAlreadyRegisteredError} to detect it and fall back to
122
+ * {@link login} or {@link requestLoginLink}.
123
+ */
124
+ async register(input) {
125
+ const data = await this.graphql.request(RegisterDocument, {
126
+ registerUserInput: input,
127
+ });
128
+ if (data.register?.token)
129
+ this.session.setToken(data.register.token);
130
+ return data.register;
131
+ }
132
+ /**
133
+ * Email-first adaptive login: does this address have password sign-in enabled?
134
+ * Public, and deliberately does not reveal whether the address is registered.
66
135
  */
67
- async devLogin(email) {
68
- const data = await this.graphql.request(DevLoginDocument, {
136
+ async checkAuthMethod(email) {
137
+ const data = await this.graphql.request(CheckAuthMethodDocument, {
69
138
  input: { email },
70
139
  });
71
- if (data.devLogin?.token)
72
- this.session.setToken(data.devLogin.token);
73
- return data.devLogin;
140
+ return data.checkAuthMethod;
74
141
  }
75
142
  /** The signed-in user's linked sign-in identities. Requires a session. */
76
143
  async myIdentities() {