@crowdedkingdoms/crowdyjs 15.0.0 → 15.1.0-test.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 +78 -4
- package/README.md +7 -6
- package/dist/domains/auth.d.ts +150 -8
- package/dist/domains/auth.d.ts.map +1 -1
- package/dist/domains/auth.js +202 -7
- package/dist/domains/crowdyStudio.d.ts.map +1 -1
- package/dist/domains/crowdyStudio.js +11 -2
- package/dist/generated/graphql.d.ts +104 -4
- package/dist/generated/graphql.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- 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,68 @@
|
|
|
1
|
+
# CrowdyJS v15.1 — password management (additive)
|
|
2
|
+
|
|
3
|
+
**Nothing breaks.** Four mutations the API has served all along are now wrapped,
|
|
4
|
+
so a game shipping this SDK has a first-class way to let a player set or change
|
|
5
|
+
a password:
|
|
6
|
+
|
|
7
|
+
| Method | Requires | For |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `auth.requestPasswordReset(email)` | nothing | "I forgot my password" |
|
|
10
|
+
| `auth.resetPassword({ token, newPassword })` | the emailed token | completing that |
|
|
11
|
+
| `auth.changePassword({ currentPassword, newPassword })` | a session **and** the current password | an ordinary change |
|
|
12
|
+
| `auth.setInitialPassword(newPassword)` | a session, and the account must have **no** password | adding a first password |
|
|
13
|
+
|
|
14
|
+
**Why four and not one.** Each is defined by what the caller has already
|
|
15
|
+
*proven* — an emailed token, the current password, or the session — and
|
|
16
|
+
collapsing any pair deletes the proof. In particular `setInitialPassword` is not
|
|
17
|
+
`changePassword` with the check removed: it **refuses** when a password already
|
|
18
|
+
exists, and that refusal is what stops a stolen session from replacing a
|
|
19
|
+
credential the owner still knows. Route on the refusal instead of retrying.
|
|
20
|
+
|
|
21
|
+
**`setInitialPassword` emails a security notification to the account address**
|
|
22
|
+
on success, and that is deliberately the mitigation rather than a refusal: a
|
|
23
|
+
stolen session can already attach durable attacker-controlled access through
|
|
24
|
+
`linkIdentity`, so refusing here would close nothing and would leave the
|
|
25
|
+
legitimate user of a magic-link or social-only account with no door at all. If
|
|
26
|
+
you build UI for this, tell the user the email is coming.
|
|
27
|
+
|
|
28
|
+
**Three new error predicates**, because the three refusals need different
|
|
29
|
+
handling and a caller should not have to work out which is which:
|
|
30
|
+
|
|
31
|
+
- `isPasswordAlreadySetError(e)` — `setInitialPassword` refused; use
|
|
32
|
+
`changePassword`.
|
|
33
|
+
- `isNoPasswordSetError(e)` — `changePassword` refused because there is none;
|
|
34
|
+
use `setInitialPassword`.
|
|
35
|
+
- `isInvalidCurrentPasswordError(e)` — the current password is wrong; ask again.
|
|
36
|
+
|
|
37
|
+
**Use these rather than reading `extensions.code` yourself, and the reason is
|
|
38
|
+
worth knowing.** Each refusal has its own code — `PASSWORD_ALREADY_SET`,
|
|
39
|
+
`PASSWORD_NOT_SET`, `INVALID_CURRENT_PASSWORD` — only from **ck-api v1.60.0**.
|
|
40
|
+
Before that release the first two shared `UNAUTHENTICATED` with a genuinely
|
|
41
|
+
expired session and the third arrived as `INTERNAL_SERVER_ERROR`, so a caller
|
|
42
|
+
keying on the code signed the user out over a typo. Each predicate accepts the
|
|
43
|
+
new code **and** the older wording, so one pinned build works against a tier on
|
|
44
|
+
either side of that line. `isAlreadyRegisteredError` gained the same treatment
|
|
45
|
+
(`EMAIL_ALREADY_REGISTERED`).
|
|
46
|
+
|
|
47
|
+
None of the three means the session is gone. Sign a user out on
|
|
48
|
+
`UNAUTHENTICATED`, which from v1.60.0 says only that.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
try {
|
|
52
|
+
await client.auth.setInitialPassword(pw);
|
|
53
|
+
} catch (e) {
|
|
54
|
+
if (isPasswordAlreadySetError(e)) {
|
|
55
|
+
// They have one already — ask for it rather than replacing it blind.
|
|
56
|
+
await client.auth.changePassword({ currentPassword: current, newPassword: pw });
|
|
57
|
+
} else throw e;
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`resetPassword` and `changePassword` do **not** revoke existing sessions. Follow
|
|
62
|
+
either with `auth.logoutAllDevices()` if that is the intent.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
1
66
|
# CrowdyJS v15 — the dev auth bypass is gone (breaking)
|
|
2
67
|
|
|
3
68
|
`client.auth.devLogin()` is **removed**, and so is the `devToken` field on
|
|
@@ -37,8 +102,9 @@ login, and two error predicates, because these two conditions are **not**
|
|
|
37
102
|
distinguishable by GraphQL error code:
|
|
38
103
|
|
|
39
104
|
- `isAlreadyRegisteredError(e)` — `register` refused because the address already
|
|
40
|
-
has an account.
|
|
41
|
-
`INTERNAL_SERVER_ERROR`, so a caller keying on
|
|
105
|
+
has an account. It carries `EMAIL_ALREADY_REGISTERED` from ck-api v1.60.0;
|
|
106
|
+
before that it arrived as `INTERNAL_SERVER_ERROR`, so a caller keying on
|
|
107
|
+
`CONFLICT` matched nothing. The predicate accepts both.
|
|
42
108
|
- `isPasswordUnconfirmedError(e)` — `login` refused because the password is real
|
|
43
109
|
but unconfirmed on an account with another verified sign-in method. The remedy
|
|
44
110
|
is the emailed link, not a different password.
|
|
@@ -523,8 +589,16 @@ validation error; every other sub-client is unaffected.
|
|
|
523
589
|
|
|
524
590
|
# CrowdyJS v8 — Passwordless & federated sign-in (BREAKING)
|
|
525
591
|
|
|
526
|
-
**
|
|
527
|
-
|
|
592
|
+
> **SUPERSEDED BY 15.0.0 — do not follow this section as current product.**
|
|
593
|
+
> Email + password sign-in came BACK in 15.0.0: `auth.login` and
|
|
594
|
+
> `auth.register` exist, and the `devLogin` bypass was removed from every tier
|
|
595
|
+
> on 2026-08-20. What is still true from v8 is that magic link and social
|
|
596
|
+
> sign-in are supported; what is false is that they are the ONLY options.
|
|
597
|
+
> This section is kept as the record of the v8 break. See the 15.0.0 notes at
|
|
598
|
+
> the top of this file.
|
|
599
|
+
|
|
600
|
+
**At v8, Crowded Kingdoms was passwordless.** Email + password login was removed
|
|
601
|
+
in that version. The v8 migration was to one of:
|
|
528
602
|
|
|
529
603
|
- **Magic link (email):**
|
|
530
604
|
```ts
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# CrowdyJS
|
|
2
2
|
|
|
3
3
|
The official browser-first TypeScript SDK for **Crowded Kingdoms**. CrowdyJS
|
|
4
|
-
gives you typed clients for the whole platform:
|
|
4
|
+
gives you typed clients for the whole platform: identity and
|
|
5
5
|
studio administration (the Management surface), world data and the abstract
|
|
6
6
|
game model (the Game surface), and the UDP proxy realtime stream — all over
|
|
7
7
|
one unified GraphQL API.
|
|
@@ -76,7 +76,7 @@ app and drive the world/UDP surface from a per-game client — see
|
|
|
76
76
|
|
|
77
77
|
## Authentication: session token vs app-scoped tokens
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
Sign-in returns an **identity session token** good for account,
|
|
80
80
|
studio admin and token minting, and **rejected for gameplay**. Each game is
|
|
81
81
|
entered with a short-lived **app-scoped token** confined to that one app, so a
|
|
82
82
|
game stack never receives the player's full session.
|
|
@@ -91,7 +91,8 @@ const overworld = createCrowdyClient({
|
|
|
91
91
|
httpUrl: apiUrl,
|
|
92
92
|
tokenStore: new BrowserLocalStorageTokenStore('crowdyjs:session'),
|
|
93
93
|
});
|
|
94
|
-
//
|
|
94
|
+
// Sign-in (email + password via auth.login / auth.register, magic link, or social/OIDC)
|
|
95
|
+
// yields the session token. There is no dev bypass: devLogin was removed in 15.0.0.
|
|
95
96
|
await overworld.auth.requestLoginLink({ email, redirectUri });
|
|
96
97
|
await overworld.auth.completeLoginLink(tokenFromLink);
|
|
97
98
|
|
|
@@ -139,7 +140,7 @@ Notes:
|
|
|
139
140
|
restoring auth from a non-default storage).
|
|
140
141
|
|
|
141
142
|
Deeper reading: [Portals & app-scoped tokens](https://docs.crowdedkingdoms.com/management-api/portals-and-app-tokens)
|
|
142
|
-
and [Sign in
|
|
143
|
+
and [Sign in](https://docs.crowdedkingdoms.com/management-api/authentication).
|
|
143
144
|
|
|
144
145
|
### Token refresh during gameplay
|
|
145
146
|
|
|
@@ -156,7 +157,7 @@ lifecycle to preserve.
|
|
|
156
157
|
|
|
157
158
|
## Game-loop lifecycle
|
|
158
159
|
|
|
159
|
-
1. Sign in
|
|
160
|
+
1. Sign in on the identity client with `client.auth` (`login` / `register`), or
|
|
160
161
|
restore a stored session with `client.session.restore()`. This yields the
|
|
161
162
|
**session token**, which gameplay rejects.
|
|
162
163
|
2. Mint an **app-scoped token** for the app (`identity.portal.mintAppToken(appId)`,
|
|
@@ -178,7 +179,7 @@ lifecycle to preserve.
|
|
|
178
179
|
|
|
179
180
|
| Sub-client | What it does |
|
|
180
181
|
|---|---|
|
|
181
|
-
| `client.auth` |
|
|
182
|
+
| `client.auth` | Sign-in: `login` / `register` (email + password), magic link, social/OIDC. Passwords: `requestPasswordReset` / `resetPassword`, `changePassword`, and `setInitialPassword` for an account created by magic link or a social provider (added in 15.1.0). Log out, and linked identities (`myIdentities`, `linkIdentity`/`unlinkIdentity`). **No dev bypass** — `devLogin` was removed in 15.0.0 and `DEV_AUTH_BYPASS` is gone from every tier. |
|
|
182
183
|
| `client.users` | `me`, `updateGamertag`, profile reads. |
|
|
183
184
|
| `client.session` | Token store, `restore()`, `getToken()`, manual `setToken()`. |
|
|
184
185
|
| `client.portal` | App-scoped token minting (`mintAppToken`) and the cross-origin PKCE entry flow (`beginEntry` / `handleAuthorizeRequest` / `completeEntry` / `refresh`). |
|
package/dist/domains/auth.d.ts
CHANGED
|
@@ -14,15 +14,27 @@ import type { AuthState } from '../auth-state.js';
|
|
|
14
14
|
* `register` have been first-class in the API throughout, and the C++ load
|
|
15
15
|
* tester has used them all along. Only this SDK pretended otherwise, and the
|
|
16
16
|
* gap sent integrators to the dev bypass — which no longer exists on any tier.
|
|
17
|
+
* The same gap ran one method deeper until 2026-08-21: password MANAGEMENT
|
|
18
|
+
* (reset, change, and adding a first password) was served by the API and
|
|
19
|
+
* wrapped here by nothing, so a game shipping this SDK had no first-class way
|
|
20
|
+
* to let a player set or change a password.
|
|
17
21
|
*
|
|
18
22
|
* Part of the management surface.
|
|
19
23
|
*
|
|
20
24
|
* **Public (no session):** {@link register}, {@link login},
|
|
21
25
|
* {@link requestLoginLink}, {@link completeLoginLink}, {@link socialLoginStart},
|
|
22
26
|
* {@link socialLoginComplete}, {@link availableLoginProviders},
|
|
23
|
-
* {@link checkAuthMethod}
|
|
24
|
-
*
|
|
25
|
-
* {@link unlinkIdentity}
|
|
27
|
+
* {@link checkAuthMethod}, {@link requestPasswordReset}, {@link resetPassword}.
|
|
28
|
+
* **Require a session:** {@link logout}, {@link logoutAllDevices},
|
|
29
|
+
* {@link myIdentities}, {@link linkIdentity}, {@link unlinkIdentity},
|
|
30
|
+
* {@link changePassword}, {@link setInitialPassword}.
|
|
31
|
+
*
|
|
32
|
+
* **Which password method:** the four are distinguished by what the caller has
|
|
33
|
+
* already proven, not by what they want to do. Signed in with a password →
|
|
34
|
+
* {@link changePassword}. Signed in with none → {@link setInitialPassword}.
|
|
35
|
+
* Not signed in, or signed in and cannot remember it →
|
|
36
|
+
* {@link requestPasswordReset} then {@link resetPassword}.
|
|
37
|
+
* {@link checkAuthMethod} answers `hasPassword` for an address before sign-in.
|
|
26
38
|
*/
|
|
27
39
|
export interface AuthUser {
|
|
28
40
|
userId: string;
|
|
@@ -47,11 +59,16 @@ export interface UserIdentity {
|
|
|
47
59
|
/**
|
|
48
60
|
* `register` refused because the address already has an account.
|
|
49
61
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
62
|
+
* `EMAIL_ALREADY_REGISTERED` from v1.60.0. Before that it arrived as
|
|
63
|
+
* `INTERNAL_SERVER_ERROR`, so a caller keying on the code matched nothing and
|
|
64
|
+
* treated a routine "this account exists" as a server fault; the wording branch
|
|
65
|
+
* is what still works against those tiers.
|
|
66
|
+
*
|
|
67
|
+
* Note what this does NOT accept: a bare `CONFLICT`. From v1.60.0 that is the
|
|
68
|
+
* code a generic 409 carries, and a generic code cannot identify a specific
|
|
69
|
+
* condition — a predicate that accepted it would report any future conflict in
|
|
70
|
+
* this mutation as "already registered". Only a code minted for this outcome
|
|
71
|
+
* will do.
|
|
55
72
|
*/
|
|
56
73
|
export declare function isAlreadyRegisteredError(error: unknown): boolean;
|
|
57
74
|
/**
|
|
@@ -59,8 +76,55 @@ export declare function isAlreadyRegisteredError(error: unknown): boolean;
|
|
|
59
76
|
* account that has another verified sign-in method. The remedy is the emailed
|
|
60
77
|
* confirmation link, not a different password — so this must not be reported to
|
|
61
78
|
* the user as "wrong password".
|
|
79
|
+
*
|
|
80
|
+
* **The one refusal here with no code of its own, and the only wording-only
|
|
81
|
+
* predicate left.** ck-api v1.60.0 gave the other four a dedicated
|
|
82
|
+
* `extensions.code`; this one is still a plain `UnauthorizedException`, so it
|
|
83
|
+
* arrives as `UNAUTHENTICATED` — the same code as an expired session, whose
|
|
84
|
+
* remedy (sign in again) is the opposite of this one's. The message is
|
|
85
|
+
* therefore the only discriminator, and the absence of a `codeOf` branch below
|
|
86
|
+
* is deliberate rather than an oversight: there is no code to read.
|
|
62
87
|
*/
|
|
63
88
|
export declare function isPasswordUnconfirmedError(error: unknown): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* {@link setInitialPassword} refused because the account already has a
|
|
91
|
+
* password. The remedy is {@link changePassword}, which verifies the current
|
|
92
|
+
* one.
|
|
93
|
+
*
|
|
94
|
+
* The refusal is deliberate and load-bearing: without it, `setInitialPassword`
|
|
95
|
+
* would be `changePassword` with the current-password check deleted, which is
|
|
96
|
+
* the check that stops a stolen session from silently locking the owner out.
|
|
97
|
+
* So a caller must route to `changePassword` rather than retrying.
|
|
98
|
+
*
|
|
99
|
+
* `PASSWORD_ALREADY_SET` from ck-api v1.60.0. Before that the schema said this
|
|
100
|
+
* "throws CONFLICT" and it reached clients as `INTERNAL_SERVER_ERROR`, so the
|
|
101
|
+
* message was the only thing to match — which is why the wording branch is
|
|
102
|
+
* still here.
|
|
103
|
+
*/
|
|
104
|
+
export declare function isPasswordAlreadySetError(error: unknown): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* {@link changePassword} refused because there is no password to change — a
|
|
107
|
+
* magic-link or social-only account. The remedy is {@link setInitialPassword}
|
|
108
|
+
* while signed in, or {@link requestPasswordReset}.
|
|
109
|
+
*
|
|
110
|
+
* `PASSWORD_NOT_SET` from ck-api v1.60.0. Before that this and
|
|
111
|
+
* {@link isInvalidCurrentPasswordError} both arrived as `UNAUTHENTICATED`,
|
|
112
|
+
* which is ALSO what an expired session looks like — so a caller keying on the
|
|
113
|
+
* code signed the user out when they had merely typed the wrong current
|
|
114
|
+
* password, or offered a password field on an account that has none. That is
|
|
115
|
+
* the defect the new codes exist to remove, and the wording branch is what
|
|
116
|
+
* still separates them on a tier that has not deployed it.
|
|
117
|
+
*/
|
|
118
|
+
export declare function isNoPasswordSetError(error: unknown): boolean;
|
|
119
|
+
/**
|
|
120
|
+
* {@link changePassword} refused because the current password is wrong. The
|
|
121
|
+
* remedy is to ask again — the session is fine.
|
|
122
|
+
*
|
|
123
|
+
* `INVALID_CURRENT_PASSWORD` (HTTP 403) from ck-api v1.60.0. See
|
|
124
|
+
* {@link isNoPasswordSetError} for what it used to be and why the wording
|
|
125
|
+
* branch stays.
|
|
126
|
+
*/
|
|
127
|
+
export declare function isInvalidCurrentPasswordError(error: unknown): boolean;
|
|
64
128
|
export declare class AuthAPI {
|
|
65
129
|
private readonly graphql;
|
|
66
130
|
private readonly session;
|
|
@@ -131,6 +195,84 @@ export declare class AuthAPI {
|
|
|
131
195
|
checkAuthMethod(email: string): Promise<{
|
|
132
196
|
hasPassword: boolean;
|
|
133
197
|
}>;
|
|
198
|
+
/**
|
|
199
|
+
* Email a password-reset link to the address. Public.
|
|
200
|
+
*
|
|
201
|
+
* Always resolves `true` whether or not the address has an account, so it
|
|
202
|
+
* cannot be used to enumerate users — which also means a `true` here is not
|
|
203
|
+
* evidence an email was sent.
|
|
204
|
+
*
|
|
205
|
+
* This is the ownership-proven way to add a password to an account that has
|
|
206
|
+
* none, and the only one for a user who is not signed in. A user who IS
|
|
207
|
+
* signed in should use {@link setInitialPassword} instead and skip the inbox.
|
|
208
|
+
*/
|
|
209
|
+
requestPasswordReset(email: string): Promise<boolean>;
|
|
210
|
+
/**
|
|
211
|
+
* Complete a password reset with the token from the emailed link. Public —
|
|
212
|
+
* the token is the authorization.
|
|
213
|
+
*
|
|
214
|
+
* Throws if the token is invalid or expired. **Existing sessions are not
|
|
215
|
+
* revoked**, so a reset does not by itself evict anyone already signed in;
|
|
216
|
+
* follow it with {@link logoutAllDevices} if that is what you want.
|
|
217
|
+
*/
|
|
218
|
+
resetPassword(input: {
|
|
219
|
+
token: string;
|
|
220
|
+
newPassword: string;
|
|
221
|
+
}): Promise<boolean>;
|
|
222
|
+
/**
|
|
223
|
+
* Change the signed-in user's password, verifying the current one. Requires a
|
|
224
|
+
* session.
|
|
225
|
+
*
|
|
226
|
+
* **This is not the method for an account that has no password** — a
|
|
227
|
+
* magic-link or social-only account, which cannot supply a current one. That
|
|
228
|
+
* is {@link setInitialPassword}, and the two are kept apart deliberately:
|
|
229
|
+
* the current-password check here is what stops a stolen session from
|
|
230
|
+
* changing a credential the owner still knows.
|
|
231
|
+
*
|
|
232
|
+
* Three outcomes need telling apart and the error CODE cannot do it, because
|
|
233
|
+
* a wrong current password, an account with no password, and an expired
|
|
234
|
+
* session all arrive as `UNAUTHENTICATED`:
|
|
235
|
+
* {@link isInvalidCurrentPasswordError} (ask again),
|
|
236
|
+
* {@link isNoPasswordSetError} (send them to `setInitialPassword`), and
|
|
237
|
+
* neither (the session is gone — sign in again).
|
|
238
|
+
*
|
|
239
|
+
* **Existing sessions are not revoked.**
|
|
240
|
+
*/
|
|
241
|
+
changePassword(input: {
|
|
242
|
+
currentPassword: string;
|
|
243
|
+
newPassword: string;
|
|
244
|
+
}): Promise<boolean>;
|
|
245
|
+
/**
|
|
246
|
+
* Add a password to the signed-in account when it does not have one yet.
|
|
247
|
+
* Requires a session.
|
|
248
|
+
*
|
|
249
|
+
* For an account created by magic link or a social provider, which until this
|
|
250
|
+
* existed had no in-product route to password sign-in at all — the only door
|
|
251
|
+
* was {@link requestPasswordReset}, an email round trip to add a credential to
|
|
252
|
+
* an account you are already signed in to. The session is the proof of account
|
|
253
|
+
* control, so **the password works immediately**: there is no confirmation
|
|
254
|
+
* email to wait for, and the password identity is written verified (an
|
|
255
|
+
* unverified one would be refused by {@link login} while another verified
|
|
256
|
+
* method exists, which is a dead end that looks like success).
|
|
257
|
+
*
|
|
258
|
+
* **Refuses when a password already exists** — {@link isPasswordAlreadySetError}
|
|
259
|
+
* detects it — rather than replacing it. Without that refusal this would be
|
|
260
|
+
* {@link changePassword} with the current-password check deleted. Route to
|
|
261
|
+
* `changePassword`, or to `requestPasswordReset` if the user has forgotten it.
|
|
262
|
+
*
|
|
263
|
+
* **A security notification is emailed to the account address** whenever this
|
|
264
|
+
* succeeds. That is the mitigation, and it is deliberately a notification
|
|
265
|
+
* rather than a refusal: a stolen session can already attach durable
|
|
266
|
+
* attacker-controlled access via {@link linkIdentity}, so refusing here would
|
|
267
|
+
* remove the legitimate user's only door without closing the class. Do not
|
|
268
|
+
* suppress or reword that email's role when you describe this to a user —
|
|
269
|
+
* "we have emailed you about this change" is part of the feature. The
|
|
270
|
+
* notification is best-effort on the server, so a `true` return is not proof
|
|
271
|
+
* the email was delivered.
|
|
272
|
+
*
|
|
273
|
+
* **Existing sessions are not revoked.**
|
|
274
|
+
*/
|
|
275
|
+
setInitialPassword(newPassword: string): Promise<boolean>;
|
|
134
276
|
/** The signed-in user's linked sign-in identities. Requires a session. */
|
|
135
277
|
myIdentities(): Promise<UserIdentity[]>;
|
|
136
278
|
/** 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,28 +1,117 @@
|
|
|
1
1
|
import { parse } from 'graphql';
|
|
2
2
|
import { LogoutAllDevicesDocument, LogoutDocument } from '../generated/graphql.js';
|
|
3
|
+
/**
|
|
4
|
+
* The `extensions.code` of the first GraphQL error, if there is one.
|
|
5
|
+
*
|
|
6
|
+
* Every predicate below asks the CODE first and the wording second, and the
|
|
7
|
+
* order is not a style choice. ck-api v1.60.0 fixed a mapping defect that made
|
|
8
|
+
* these codes unusable: `@nestjs/apollo` translates four HTTP statuses and
|
|
9
|
+
* collapses the rest, so a 409 arrived as `INTERNAL_SERVER_ERROR` and both of
|
|
10
|
+
* `changePassword`'s refusals arrived as `UNAUTHENTICATED` — the same code an
|
|
11
|
+
* expired session produces. The wording fallback is therefore not legacy
|
|
12
|
+
* clutter: a tier that has not deployed v1.60.0 still answers the old way, and
|
|
13
|
+
* a game pinning this SDK exactly may meet either. Delete it when no tier
|
|
14
|
+
* predates v1.60.0, and not before.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* `extensions.code` off anything error-shaped, whether it is a
|
|
18
|
+
* {@link CrowdyGraphQLError}, one raw GraphQL error entry, or the `code` getter
|
|
19
|
+
* the former lifts to the top level — all three reach a caller here, depending
|
|
20
|
+
* on whether the error was rethrown or destructured on the way.
|
|
21
|
+
*/
|
|
22
|
+
function codeOf(error) {
|
|
23
|
+
const shape = error;
|
|
24
|
+
if (typeof shape?.extensions?.code === 'string')
|
|
25
|
+
return shape.extensions.code;
|
|
26
|
+
return typeof shape?.code === 'string' ? shape.code : undefined;
|
|
27
|
+
}
|
|
28
|
+
function messageOf(error) {
|
|
29
|
+
return error instanceof Error ? error.message : String(error);
|
|
30
|
+
}
|
|
3
31
|
/**
|
|
4
32
|
* `register` refused because the address already has an account.
|
|
5
33
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
34
|
+
* `EMAIL_ALREADY_REGISTERED` from v1.60.0. Before that it arrived as
|
|
35
|
+
* `INTERNAL_SERVER_ERROR`, so a caller keying on the code matched nothing and
|
|
36
|
+
* treated a routine "this account exists" as a server fault; the wording branch
|
|
37
|
+
* is what still works against those tiers.
|
|
38
|
+
*
|
|
39
|
+
* Note what this does NOT accept: a bare `CONFLICT`. From v1.60.0 that is the
|
|
40
|
+
* code a generic 409 carries, and a generic code cannot identify a specific
|
|
41
|
+
* condition — a predicate that accepted it would report any future conflict in
|
|
42
|
+
* this mutation as "already registered". Only a code minted for this outcome
|
|
43
|
+
* will do.
|
|
11
44
|
*/
|
|
12
45
|
export function isAlreadyRegisteredError(error) {
|
|
13
|
-
|
|
14
|
-
|
|
46
|
+
return (codeOf(error) === 'EMAIL_ALREADY_REGISTERED' ||
|
|
47
|
+
/account with this email already exists/i.test(messageOf(error)));
|
|
15
48
|
}
|
|
16
49
|
/**
|
|
17
50
|
* `login` refused because the password is real but not yet confirmed, on an
|
|
18
51
|
* account that has another verified sign-in method. The remedy is the emailed
|
|
19
52
|
* confirmation link, not a different password — so this must not be reported to
|
|
20
53
|
* the user as "wrong password".
|
|
54
|
+
*
|
|
55
|
+
* **The one refusal here with no code of its own, and the only wording-only
|
|
56
|
+
* predicate left.** ck-api v1.60.0 gave the other four a dedicated
|
|
57
|
+
* `extensions.code`; this one is still a plain `UnauthorizedException`, so it
|
|
58
|
+
* arrives as `UNAUTHENTICATED` — the same code as an expired session, whose
|
|
59
|
+
* remedy (sign in again) is the opposite of this one's. The message is
|
|
60
|
+
* therefore the only discriminator, and the absence of a `codeOf` branch below
|
|
61
|
+
* is deliberate rather than an oversight: there is no code to read.
|
|
21
62
|
*/
|
|
22
63
|
export function isPasswordUnconfirmedError(error) {
|
|
23
64
|
const message = error instanceof Error ? error.message : String(error);
|
|
24
65
|
return /confirm your email to enable password sign-in/i.test(message);
|
|
25
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* {@link setInitialPassword} refused because the account already has a
|
|
69
|
+
* password. The remedy is {@link changePassword}, which verifies the current
|
|
70
|
+
* one.
|
|
71
|
+
*
|
|
72
|
+
* The refusal is deliberate and load-bearing: without it, `setInitialPassword`
|
|
73
|
+
* would be `changePassword` with the current-password check deleted, which is
|
|
74
|
+
* the check that stops a stolen session from silently locking the owner out.
|
|
75
|
+
* So a caller must route to `changePassword` rather than retrying.
|
|
76
|
+
*
|
|
77
|
+
* `PASSWORD_ALREADY_SET` from ck-api v1.60.0. Before that the schema said this
|
|
78
|
+
* "throws CONFLICT" and it reached clients as `INTERNAL_SERVER_ERROR`, so the
|
|
79
|
+
* message was the only thing to match — which is why the wording branch is
|
|
80
|
+
* still here.
|
|
81
|
+
*/
|
|
82
|
+
export function isPasswordAlreadySetError(error) {
|
|
83
|
+
return (codeOf(error) === 'PASSWORD_ALREADY_SET' ||
|
|
84
|
+
/this account already has a password/i.test(messageOf(error)));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* {@link changePassword} refused because there is no password to change — a
|
|
88
|
+
* magic-link or social-only account. The remedy is {@link setInitialPassword}
|
|
89
|
+
* while signed in, or {@link requestPasswordReset}.
|
|
90
|
+
*
|
|
91
|
+
* `PASSWORD_NOT_SET` from ck-api v1.60.0. Before that this and
|
|
92
|
+
* {@link isInvalidCurrentPasswordError} both arrived as `UNAUTHENTICATED`,
|
|
93
|
+
* which is ALSO what an expired session looks like — so a caller keying on the
|
|
94
|
+
* code signed the user out when they had merely typed the wrong current
|
|
95
|
+
* password, or offered a password field on an account that has none. That is
|
|
96
|
+
* the defect the new codes exist to remove, and the wording branch is what
|
|
97
|
+
* still separates them on a tier that has not deployed it.
|
|
98
|
+
*/
|
|
99
|
+
export function isNoPasswordSetError(error) {
|
|
100
|
+
return (codeOf(error) === 'PASSWORD_NOT_SET' ||
|
|
101
|
+
/no password is set on this account/i.test(messageOf(error)));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* {@link changePassword} refused because the current password is wrong. The
|
|
105
|
+
* remedy is to ask again — the session is fine.
|
|
106
|
+
*
|
|
107
|
+
* `INVALID_CURRENT_PASSWORD` (HTTP 403) from ck-api v1.60.0. See
|
|
108
|
+
* {@link isNoPasswordSetError} for what it used to be and why the wording
|
|
109
|
+
* branch stays.
|
|
110
|
+
*/
|
|
111
|
+
export function isInvalidCurrentPasswordError(error) {
|
|
112
|
+
return (codeOf(error) === 'INVALID_CURRENT_PASSWORD' ||
|
|
113
|
+
/invalid current password/i.test(messageOf(error)));
|
|
114
|
+
}
|
|
26
115
|
const AUTH_RESPONSE_FIELDS = 'token gameTokenId user { userId email gamertag }';
|
|
27
116
|
const IDENTITY_FIELDS = 'identityId provider subject email emailVerified createdAt lastLoginAt';
|
|
28
117
|
// `devToken` is NOT selected. The field returned the magic-link token in
|
|
@@ -40,6 +129,22 @@ const SocialLoginCompleteDocument = parse(`mutation SocialLoginComplete($input:
|
|
|
40
129
|
// naming a field the caller never wrote.
|
|
41
130
|
const LoginDocument = parse(`mutation Login($loginUserInput: LoginUserInput!) { login(loginUserInput: $loginUserInput) { ${AUTH_RESPONSE_FIELDS} } }`);
|
|
42
131
|
const RegisterDocument = parse(`mutation Register($registerUserInput: RegisterUserInput!) { register(registerUserInput: $registerUserInput) { ${AUTH_RESPONSE_FIELDS} } }`);
|
|
132
|
+
// PASSWORD MANAGEMENT. Four mutations, and they are four rather than one or two
|
|
133
|
+
// on purpose: each is defined by what the CALLER has already proven, and
|
|
134
|
+
// collapsing any pair would delete the proof.
|
|
135
|
+
//
|
|
136
|
+
// requestPasswordReset / resetPassword proof = the emailed token
|
|
137
|
+
// changePassword proof = the current password
|
|
138
|
+
// setInitialPassword proof = the session, and there is no
|
|
139
|
+
// password to verify
|
|
140
|
+
//
|
|
141
|
+
// `changePassword` takes its two arguments FLAT rather than in an input object,
|
|
142
|
+
// and `resetPassword` takes `resetPasswordInput` rather than `input`. Both are
|
|
143
|
+
// the server's spelling; see the note above `LoginDocument`.
|
|
144
|
+
const RequestPasswordResetDocument = parse(`mutation RequestPasswordReset($email: String!) { requestPasswordReset(email: $email) }`);
|
|
145
|
+
const ResetPasswordDocument = parse(`mutation ResetPassword($resetPasswordInput: ResetPasswordInput!) { resetPassword(resetPasswordInput: $resetPasswordInput) }`);
|
|
146
|
+
const ChangePasswordDocument = parse(`mutation ChangePassword($currentPassword: String!, $newPassword: String!) { changePassword(currentPassword: $currentPassword, newPassword: $newPassword) }`);
|
|
147
|
+
const SetInitialPasswordDocument = parse(`mutation SetInitialPassword($newPassword: String!) { setInitialPassword(newPassword: $newPassword) }`);
|
|
43
148
|
const CheckAuthMethodDocument = parse(`query CheckAuthMethod($input: CheckAuthMethodInput!) { checkAuthMethod(input: $input) { hasPassword } }`);
|
|
44
149
|
const AvailableLoginProvidersDocument = parse(`query AvailableLoginProviders { availableLoginProviders }`);
|
|
45
150
|
const MyIdentitiesDocument = parse(`query MyIdentities { myIdentities { ${IDENTITY_FIELDS} } }`);
|
|
@@ -139,6 +244,96 @@ export class AuthAPI {
|
|
|
139
244
|
});
|
|
140
245
|
return data.checkAuthMethod;
|
|
141
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Email a password-reset link to the address. Public.
|
|
249
|
+
*
|
|
250
|
+
* Always resolves `true` whether or not the address has an account, so it
|
|
251
|
+
* cannot be used to enumerate users — which also means a `true` here is not
|
|
252
|
+
* evidence an email was sent.
|
|
253
|
+
*
|
|
254
|
+
* This is the ownership-proven way to add a password to an account that has
|
|
255
|
+
* none, and the only one for a user who is not signed in. A user who IS
|
|
256
|
+
* signed in should use {@link setInitialPassword} instead and skip the inbox.
|
|
257
|
+
*/
|
|
258
|
+
async requestPasswordReset(email) {
|
|
259
|
+
const data = await this.graphql.request(RequestPasswordResetDocument, {
|
|
260
|
+
email,
|
|
261
|
+
});
|
|
262
|
+
return data.requestPasswordReset;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Complete a password reset with the token from the emailed link. Public —
|
|
266
|
+
* the token is the authorization.
|
|
267
|
+
*
|
|
268
|
+
* Throws if the token is invalid or expired. **Existing sessions are not
|
|
269
|
+
* revoked**, so a reset does not by itself evict anyone already signed in;
|
|
270
|
+
* follow it with {@link logoutAllDevices} if that is what you want.
|
|
271
|
+
*/
|
|
272
|
+
async resetPassword(input) {
|
|
273
|
+
const data = await this.graphql.request(ResetPasswordDocument, {
|
|
274
|
+
resetPasswordInput: input,
|
|
275
|
+
});
|
|
276
|
+
return data.resetPassword;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Change the signed-in user's password, verifying the current one. Requires a
|
|
280
|
+
* session.
|
|
281
|
+
*
|
|
282
|
+
* **This is not the method for an account that has no password** — a
|
|
283
|
+
* magic-link or social-only account, which cannot supply a current one. That
|
|
284
|
+
* is {@link setInitialPassword}, and the two are kept apart deliberately:
|
|
285
|
+
* the current-password check here is what stops a stolen session from
|
|
286
|
+
* changing a credential the owner still knows.
|
|
287
|
+
*
|
|
288
|
+
* Three outcomes need telling apart and the error CODE cannot do it, because
|
|
289
|
+
* a wrong current password, an account with no password, and an expired
|
|
290
|
+
* session all arrive as `UNAUTHENTICATED`:
|
|
291
|
+
* {@link isInvalidCurrentPasswordError} (ask again),
|
|
292
|
+
* {@link isNoPasswordSetError} (send them to `setInitialPassword`), and
|
|
293
|
+
* neither (the session is gone — sign in again).
|
|
294
|
+
*
|
|
295
|
+
* **Existing sessions are not revoked.**
|
|
296
|
+
*/
|
|
297
|
+
async changePassword(input) {
|
|
298
|
+
const data = await this.graphql.request(ChangePasswordDocument, input);
|
|
299
|
+
return data.changePassword;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Add a password to the signed-in account when it does not have one yet.
|
|
303
|
+
* Requires a session.
|
|
304
|
+
*
|
|
305
|
+
* For an account created by magic link or a social provider, which until this
|
|
306
|
+
* existed had no in-product route to password sign-in at all — the only door
|
|
307
|
+
* was {@link requestPasswordReset}, an email round trip to add a credential to
|
|
308
|
+
* an account you are already signed in to. The session is the proof of account
|
|
309
|
+
* control, so **the password works immediately**: there is no confirmation
|
|
310
|
+
* email to wait for, and the password identity is written verified (an
|
|
311
|
+
* unverified one would be refused by {@link login} while another verified
|
|
312
|
+
* method exists, which is a dead end that looks like success).
|
|
313
|
+
*
|
|
314
|
+
* **Refuses when a password already exists** — {@link isPasswordAlreadySetError}
|
|
315
|
+
* detects it — rather than replacing it. Without that refusal this would be
|
|
316
|
+
* {@link changePassword} with the current-password check deleted. Route to
|
|
317
|
+
* `changePassword`, or to `requestPasswordReset` if the user has forgotten it.
|
|
318
|
+
*
|
|
319
|
+
* **A security notification is emailed to the account address** whenever this
|
|
320
|
+
* succeeds. That is the mitigation, and it is deliberately a notification
|
|
321
|
+
* rather than a refusal: a stolen session can already attach durable
|
|
322
|
+
* attacker-controlled access via {@link linkIdentity}, so refusing here would
|
|
323
|
+
* remove the legitimate user's only door without closing the class. Do not
|
|
324
|
+
* suppress or reword that email's role when you describe this to a user —
|
|
325
|
+
* "we have emailed you about this change" is part of the feature. The
|
|
326
|
+
* notification is best-effort on the server, so a `true` return is not proof
|
|
327
|
+
* the email was delivered.
|
|
328
|
+
*
|
|
329
|
+
* **Existing sessions are not revoked.**
|
|
330
|
+
*/
|
|
331
|
+
async setInitialPassword(newPassword) {
|
|
332
|
+
const data = await this.graphql.request(SetInitialPasswordDocument, {
|
|
333
|
+
newPassword,
|
|
334
|
+
});
|
|
335
|
+
return data.setInitialPassword;
|
|
336
|
+
}
|
|
142
337
|
/** The signed-in user's linked sign-in identities. Requires a session. */
|
|
143
338
|
async myIdentities() {
|
|
144
339
|
const data = await this.graphql.request(MyIdentitiesDocument);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crowdyStudio.d.ts","sourceRoot":"","sources":["../../src/domains/crowdyStudio.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAQlD,OAAO,EAIL,KAAK,8BAA8B,EACnC,KAAK,oCAAoC,EAEzC,KAAK,mBAAmB,EAExB,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC7B,KAAK,0BAA0B,EAC/B,KAAK,yBAAyB,EAC9B,KAAK,gCAAgC,EACrC,KAAK,4BAA4B,EAClC,MAAM,4BAA4B,CAAC;AA0BpC;;;;;;GAMG;AACH,qBAAa,eAAgB,YAAW,2BAA2B;IAGrD,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0C;gBAEvC,OAAO,EAAE,aAAa;IAE7C,YAAY,CAChB,KAAK,EAAE,wBAAwB,GAC9B,OAAO,CAAC,0BAA0B,EAAE,CAAC;IAUlC,UAAU,CACd,KAAK,EAAE,wBAAwB,GAAG;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,GACtD,OAAO,CAAC,mBAAmB,CAAC;IAQzB,aAAa,CACjB,KAAK,EAAE,8BAA8B,GACpC,OAAO,CAAC,mBAAmB,CAAC;IAoBzB,WAAW,CACf,KAAK,EAAE,4BAA4B,GAClC,OAAO,CAAC,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"crowdyStudio.d.ts","sourceRoot":"","sources":["../../src/domains/crowdyStudio.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAQlD,OAAO,EAIL,KAAK,8BAA8B,EACnC,KAAK,oCAAoC,EAEzC,KAAK,mBAAmB,EAExB,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC7B,KAAK,0BAA0B,EAC/B,KAAK,yBAAyB,EAC9B,KAAK,gCAAgC,EACrC,KAAK,4BAA4B,EAClC,MAAM,4BAA4B,CAAC;AA0BpC;;;;;;GAMG;AACH,qBAAa,eAAgB,YAAW,2BAA2B;IAGrD,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0C;gBAEvC,OAAO,EAAE,aAAa;IAE7C,YAAY,CAChB,KAAK,EAAE,wBAAwB,GAC9B,OAAO,CAAC,0BAA0B,EAAE,CAAC;IAUlC,UAAU,CACd,KAAK,EAAE,wBAAwB,GAAG;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,GACtD,OAAO,CAAC,mBAAmB,CAAC;IAQzB,aAAa,CACjB,KAAK,EAAE,8BAA8B,GACpC,OAAO,CAAC,mBAAmB,CAAC;IAoBzB,WAAW,CACf,KAAK,EAAE,4BAA4B,GAClC,OAAO,CAAC,mBAAmB,CAAC;IAiEzB,wBAAwB,CAC5B,KAAK,EAAE,wBAAwB,GAC9B,OAAO,CAAC,yBAAyB,EAAE,CAAC;IAUjC,uBAAuB,CAC3B,KAAK,EAAE,gCAAgC,GACtC,OAAO,CAAC,yBAAyB,CAAC;IAc/B,eAAe,CACnB,KAAK,EAAE,wBAAwB,GAC9B,OAAO,CAAC,yBAAyB,EAAE,CAAC;IASjC,mBAAmB,CACvB,KAAK,EAAE,oCAAoC,GAC1C,OAAO,CAAC,mBAAmB,CAAC;IAuB/B,OAAO,CAAC,QAAQ;YAKF,OAAO;CAiBtB"}
|
|
@@ -76,9 +76,18 @@ export class CrowdyStudioAPI {
|
|
|
76
76
|
return this.remember(fromProjectDto(data.crowdyStudioProjectSave, input.gridId));
|
|
77
77
|
}
|
|
78
78
|
catch (error) {
|
|
79
|
+
// THE CODE IS `CROWDY_STUDIO_REVISION_CONFLICT`, not `CONFLICT`. This
|
|
80
|
+
// asked for `CONFLICT` and could therefore never match, so a real remote
|
|
81
|
+
// conflict never became a `CrowdyStudioRevisionConflictError` and the
|
|
82
|
+
// editor's "the remote moved" recovery — refetch, then offer to keep your
|
|
83
|
+
// version — was unreachable from the server side. The SDL description is
|
|
84
|
+
// where it came from: it said "returns CONFLICT with
|
|
85
|
+
// CROWDY_STUDIO_REVISION_CONFLICT", which reads as a code plus a detail
|
|
86
|
+
// and is one code with a long name. `crowdy-agent/graphql-transport.ts`
|
|
87
|
+
// had it right all along, in this same package.
|
|
79
88
|
if (error instanceof CrowdyGraphQLError &&
|
|
80
|
-
error.code === '
|
|
81
|
-
|
|
89
|
+
(error.code === 'CROWDY_STUDIO_REVISION_CONFLICT' ||
|
|
90
|
+
error.message.includes('CROWDY_STUDIO_REVISION_CONFLICT'))) {
|
|
82
91
|
let remoteProject;
|
|
83
92
|
try {
|
|
84
93
|
remoteProject = await this.getProject({
|