@crowdedkingdoms/crowdyjs 14.2.0-test.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 +54 -1
- package/README.md +9 -7
- package/dist/domains/auth.d.ts +69 -16
- package/dist/domains/auth.d.ts.map +1 -1
- package/dist/domains/auth.js +80 -13
- package/dist/generated/graphql.d.ts +243 -34
- package/dist/generated/graphql.d.ts.map +1 -1
- package/dist/generated/graphql.js +7 -1
- package/dist/index.d.ts +15 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -9
- package/dist/kit/npcs.d.ts.map +1 -1
- package/dist/kit/social.d.ts.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
63
|
-
//
|
|
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>.
|
|
729
|
-
CROWDY_WS_URL='wss://ck.<tier>.
|
|
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
|
package/dist/domains/auth.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
15
|
-
* {@link
|
|
16
|
-
* {@link
|
|
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).
|
|
49
|
-
*
|
|
50
|
-
* token
|
|
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
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* Throws
|
|
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
|
-
|
|
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
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/domains/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/domains/auth.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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).
|
|
27
|
-
*
|
|
28
|
-
* token
|
|
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
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* Throws
|
|
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
|
|
68
|
-
const data = await this.graphql.request(
|
|
136
|
+
async checkAuthMethod(email) {
|
|
137
|
+
const data = await this.graphql.request(CheckAuthMethodDocument, {
|
|
69
138
|
input: { email },
|
|
70
139
|
});
|
|
71
|
-
|
|
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() {
|