@crowdedkingdoms/crowdyjs 7.1.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +46 -0
- package/README.md +27 -20
- package/dist/domains/auth.d.ts +77 -140
- package/dist/domains/auth.d.ts.map +1 -1
- package/dist/domains/auth.js +81 -178
- package/dist/domains/portal.d.ts +43 -1
- package/dist/domains/portal.d.ts.map +1 -1
- package/dist/domains/portal.js +59 -1
- package/dist/generated/graphql.d.ts +178 -147
- package/dist/generated/graphql.d.ts.map +1 -1
- package/dist/generated/graphql.js +0 -7
- package/dist/index.d.ts +13 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -8
- package/package.json +1 -1
package/dist/domains/auth.js
CHANGED
|
@@ -1,207 +1,110 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* without you threading the token through by hand. Use {@link setToken} to
|
|
15
|
-
* rehydrate a saved token and {@link getToken} to read the current one. `BigInt`
|
|
16
|
-
* ids on the returned user (e.g. `userId`, `orgId`) are decimal strings.
|
|
17
|
-
*
|
|
18
|
-
* **Public — no session required:** {@link login}, {@link register},
|
|
19
|
-
* {@link confirmEmail}, {@link requestPasswordReset}, {@link resetPassword}, and
|
|
20
|
-
* {@link resendConfirmationEmail}. **Require a valid session:** {@link logout},
|
|
21
|
-
* {@link logoutAllDevices}, and {@link changePassword}, which otherwise throw
|
|
22
|
-
* {@link CrowdyGraphQLError} with `UNAUTHENTICATED` when the bearer token is
|
|
23
|
-
* missing, expired, or revoked.
|
|
24
|
-
*/
|
|
1
|
+
import { parse } from 'graphql';
|
|
2
|
+
import { LogoutAllDevicesDocument, LogoutDocument } from '../generated/graphql.js';
|
|
3
|
+
const AUTH_RESPONSE_FIELDS = 'token gameTokenId user { userId email gamertag }';
|
|
4
|
+
const IDENTITY_FIELDS = 'identityId provider subject email emailVerified createdAt lastLoginAt';
|
|
5
|
+
const RequestLoginLinkDocument = parse(`mutation RequestLoginLink($input: RequestLoginLinkInput!) { requestLoginLink(input: $input) { sent devToken } }`);
|
|
6
|
+
const CompleteLoginLinkDocument = parse(`mutation CompleteLoginLink($input: CompleteLoginLinkInput!) { completeLoginLink(input: $input) { ${AUTH_RESPONSE_FIELDS} } }`);
|
|
7
|
+
const SocialLoginStartDocument = parse(`mutation SocialLoginStart($input: SocialLoginStartInput!) { socialLoginStart(input: $input) { authorizeUrl state } }`);
|
|
8
|
+
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} } }`);
|
|
10
|
+
const AvailableLoginProvidersDocument = parse(`query AvailableLoginProviders { availableLoginProviders }`);
|
|
11
|
+
const MyIdentitiesDocument = parse(`query MyIdentities { myIdentities { ${IDENTITY_FIELDS} } }`);
|
|
12
|
+
const LinkIdentityDocument = parse(`mutation LinkIdentity($input: LinkIdentityInput!) { linkIdentity(input: $input) { ${IDENTITY_FIELDS} } }`);
|
|
13
|
+
const UnlinkIdentityDocument = parse(`mutation UnlinkIdentity($identityId: String!) { unlinkIdentity(identityId: $identityId) }`);
|
|
25
14
|
export class AuthAPI {
|
|
26
15
|
constructor(graphql, session) {
|
|
27
16
|
this.graphql = graphql;
|
|
28
17
|
this.session = session;
|
|
29
18
|
}
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
* On success the returned `token` is minted **and** stored on the shared
|
|
35
|
-
* session state, so subsequent calls on any sub-client (management-api or
|
|
36
|
-
* game-api) carry it automatically — no need to call {@link setToken}.
|
|
37
|
-
*
|
|
38
|
-
* @param input - Credentials ({@link LoginUserInput}): `email` and `password`
|
|
39
|
-
* (min 8 characters).
|
|
40
|
-
* @returns An {@link AuthResponse}: the opaque session `token` (sent as
|
|
41
|
-
* `Authorization: Bearer <token>`), `gameTokenId` (the session row id, a
|
|
42
|
-
* string), and the authenticated `user`.
|
|
43
|
-
* @throws {CrowdyGraphQLError} `UNAUTHENTICATED` on invalid credentials, or
|
|
44
|
-
* `BAD_USER_INPUT` on malformed input.
|
|
45
|
-
* @example
|
|
46
|
-
* ```ts
|
|
47
|
-
* const { user } = await client.auth.login({ email, password });
|
|
48
|
-
* // the session token is now stored; later calls are authenticated for you
|
|
49
|
-
* await client.users.me();
|
|
50
|
-
* ```
|
|
51
|
-
*/
|
|
52
|
-
async login(input) {
|
|
53
|
-
const data = await this.graphql.request(LoginDocument, { input });
|
|
54
|
-
if (data.login?.token) {
|
|
55
|
-
this.session.setToken(data.login.token);
|
|
56
|
-
}
|
|
57
|
-
return data.login;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Create a new (initially unconfirmed) account, send a confirmation email, and
|
|
61
|
-
* return a session for immediate login. **Public** — no existing session
|
|
62
|
-
* required. Same token-persistence behaviour as {@link login}: the new `token`
|
|
63
|
-
* is stored on the shared session state automatically.
|
|
64
|
-
*
|
|
65
|
-
* @param input - New-account details ({@link RegisterUserInput}): `email`
|
|
66
|
-
* (where the confirmation email is sent), `password` (min 8 characters), and
|
|
67
|
-
* an optional initial `gamertag` (min 3 characters; can also be set later via
|
|
68
|
-
* `client.users.updateGamertag`).
|
|
69
|
-
* @returns An {@link AuthResponse} (session `token`, `gameTokenId`, and the new
|
|
70
|
-
* `user`).
|
|
71
|
-
* @throws {CrowdyGraphQLError} `BAD_USER_INPUT` if the email already exists or
|
|
72
|
-
* the input is invalid.
|
|
73
|
-
*/
|
|
74
|
-
async register(input) {
|
|
75
|
-
const data = await this.graphql.request(RegisterDocument, { input });
|
|
76
|
-
if (data.register?.token) {
|
|
77
|
-
this.session.setToken(data.register.token);
|
|
78
|
-
}
|
|
79
|
-
return data.register;
|
|
19
|
+
/** The federated sign-in providers currently enabled (e.g. `['google']`). */
|
|
20
|
+
async availableLoginProviders() {
|
|
21
|
+
const data = await this.graphql.request(AvailableLoginProvidersDocument);
|
|
22
|
+
return data.availableLoginProviders;
|
|
80
23
|
}
|
|
81
24
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* @returns `true` if a token was revoked, or `false` if the request carried no
|
|
88
|
-
* game token.
|
|
89
|
-
* @throws {CrowdyGraphQLError} `UNAUTHENTICATED` if the session is invalid.
|
|
25
|
+
* 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.
|
|
90
29
|
*/
|
|
91
|
-
async
|
|
92
|
-
const data = await this.graphql.request(
|
|
93
|
-
|
|
94
|
-
return data.logout;
|
|
30
|
+
async requestLoginLink(input) {
|
|
31
|
+
const data = await this.graphql.request(RequestLoginLinkDocument, { input });
|
|
32
|
+
return data.requestLoginLink;
|
|
95
33
|
}
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
* @returns `true` on success.
|
|
105
|
-
* @throws {CrowdyGraphQLError} `UNAUTHENTICATED` if the session is invalid.
|
|
106
|
-
*/
|
|
107
|
-
async logoutAllDevices() {
|
|
108
|
-
const data = await this.graphql.request(LogoutAllDevicesDocument);
|
|
109
|
-
return data.logoutAllDevices;
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Confirm a user's email address using the token from the confirmation email.
|
|
113
|
-
* **Public** — the token itself authorizes the call.
|
|
114
|
-
*
|
|
115
|
-
* @param token - The confirmation token from the emailed link.
|
|
116
|
-
* @returns `true` on success, or `false` if the token is invalid or expired.
|
|
117
|
-
* @throws {CrowdyGraphQLError} on transport/validation failures (invalid or
|
|
118
|
-
* expired tokens resolve to `false` rather than throwing).
|
|
119
|
-
*/
|
|
120
|
-
async confirmEmail(token) {
|
|
121
|
-
const data = await this.graphql.request(ConfirmEmailDocument, { token });
|
|
122
|
-
return data.confirmEmail;
|
|
34
|
+
/** Complete a magic-link sign-in; stores the session token on success. */
|
|
35
|
+
async completeLoginLink(token) {
|
|
36
|
+
const data = await this.graphql.request(CompleteLoginLinkDocument, {
|
|
37
|
+
input: { token },
|
|
38
|
+
});
|
|
39
|
+
if (data.completeLoginLink?.token)
|
|
40
|
+
this.session.setToken(data.completeLoginLink.token);
|
|
41
|
+
return data.completeLoginLink;
|
|
123
42
|
}
|
|
124
43
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* is confirmed, to prevent account enumeration.
|
|
128
|
-
*
|
|
129
|
-
* @param email - Email address to send the password-reset link to.
|
|
130
|
-
* @returns `true` (always, even when no such account exists).
|
|
131
|
-
* @throws {CrowdyGraphQLError} on transport/validation failures.
|
|
44
|
+
* Begin a federated (social) sign-in. Returns an `authorizeUrl` to redirect the
|
|
45
|
+
* user to and an opaque `state` to round-trip back to {@link socialLoginComplete}.
|
|
132
46
|
*/
|
|
133
|
-
async
|
|
134
|
-
const data = await this.graphql.request(
|
|
135
|
-
|
|
47
|
+
async socialLoginStart(provider, redirectUri) {
|
|
48
|
+
const data = await this.graphql.request(SocialLoginStartDocument, {
|
|
49
|
+
input: { provider, redirectUri },
|
|
136
50
|
});
|
|
137
|
-
return data.
|
|
51
|
+
return data.socialLoginStart;
|
|
138
52
|
}
|
|
139
|
-
/**
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
* **not** revoked.
|
|
143
|
-
*
|
|
144
|
-
* @param input - {@link ResetPasswordInput}: the `token` from the emailed reset
|
|
145
|
-
* link and the `newPassword` to set (min 8 characters).
|
|
146
|
-
* @returns `true` on success.
|
|
147
|
-
* @throws {CrowdyGraphQLError} `BAD_USER_INPUT` if the token is invalid or
|
|
148
|
-
* expired.
|
|
149
|
-
*/
|
|
150
|
-
async resetPassword(input) {
|
|
151
|
-
const data = await this.graphql.request(ResetPasswordDocument, {
|
|
53
|
+
/** Complete a federated sign-in from the provider callback; stores the token. */
|
|
54
|
+
async socialLoginComplete(input) {
|
|
55
|
+
const data = await this.graphql.request(SocialLoginCompleteDocument, {
|
|
152
56
|
input,
|
|
153
57
|
});
|
|
154
|
-
|
|
58
|
+
if (data.socialLoginComplete?.token)
|
|
59
|
+
this.session.setToken(data.socialLoginComplete.token);
|
|
60
|
+
return data.socialLoginComplete;
|
|
155
61
|
}
|
|
156
62
|
/**
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
* accounts.
|
|
161
|
-
*
|
|
162
|
-
* @param email - Email address of the account to re-send confirmation to.
|
|
163
|
-
* @returns `true` (always).
|
|
164
|
-
* @throws {CrowdyGraphQLError} on transport/validation failures.
|
|
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).
|
|
165
66
|
*/
|
|
166
|
-
async
|
|
167
|
-
const data = await this.graphql.request(
|
|
168
|
-
email,
|
|
67
|
+
async devLogin(email) {
|
|
68
|
+
const data = await this.graphql.request(DevLoginDocument, {
|
|
69
|
+
input: { email },
|
|
169
70
|
});
|
|
170
|
-
|
|
71
|
+
if (data.devLogin?.token)
|
|
72
|
+
this.session.setToken(data.devLogin.token);
|
|
73
|
+
return data.devLogin;
|
|
171
74
|
}
|
|
172
|
-
/**
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
75
|
+
/** The signed-in user's linked sign-in identities. Requires a session. */
|
|
76
|
+
async myIdentities() {
|
|
77
|
+
const data = await this.graphql.request(MyIdentitiesDocument);
|
|
78
|
+
return data.myIdentities;
|
|
79
|
+
}
|
|
80
|
+
/** Link an additional federated identity (from a social callback). */
|
|
81
|
+
async linkIdentity(input) {
|
|
82
|
+
const data = await this.graphql.request(LinkIdentityDocument, { input });
|
|
83
|
+
return data.linkIdentity;
|
|
84
|
+
}
|
|
85
|
+
/** Unlink a federated identity (cannot remove the last sign-in method). */
|
|
86
|
+
async unlinkIdentity(identityId) {
|
|
87
|
+
const data = await this.graphql.request(UnlinkIdentityDocument, {
|
|
88
|
+
identityId,
|
|
186
89
|
});
|
|
187
|
-
return data.
|
|
90
|
+
return data.unlinkIdentity;
|
|
188
91
|
}
|
|
189
|
-
/**
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
92
|
+
/** Single-device logout; clears the in-memory token on success. */
|
|
93
|
+
async logout() {
|
|
94
|
+
const data = await this.graphql.request(LogoutDocument);
|
|
95
|
+
this.session.setToken(null);
|
|
96
|
+
return data.logout;
|
|
97
|
+
}
|
|
98
|
+
/** Revoke every active session for the user. Requires a session. */
|
|
99
|
+
async logoutAllDevices() {
|
|
100
|
+
const data = await this.graphql.request(LogoutAllDevicesDocument);
|
|
101
|
+
return data.logoutAllDevices;
|
|
102
|
+
}
|
|
103
|
+
/** Imperatively set the in-memory bearer token (e.g. rehydrate). */
|
|
196
104
|
setToken(token) {
|
|
197
105
|
this.session.setToken(token);
|
|
198
106
|
}
|
|
199
|
-
/**
|
|
200
|
-
* Read the current in-memory bearer token from the shared session state. Local
|
|
201
|
-
* only — performs no network call.
|
|
202
|
-
*
|
|
203
|
-
* @returns The current bearer token, or `null` if none is set.
|
|
204
|
-
*/
|
|
107
|
+
/** Read the current in-memory bearer token. */
|
|
205
108
|
getToken() {
|
|
206
109
|
return this.session.getToken();
|
|
207
110
|
}
|
package/dist/domains/portal.d.ts
CHANGED
|
@@ -60,6 +60,30 @@ export declare class BrowserSessionPkceStore implements PkceStore {
|
|
|
60
60
|
set(state: string, verifier: string): void;
|
|
61
61
|
remove(state: string): void;
|
|
62
62
|
}
|
|
63
|
+
export interface PortalConsentState {
|
|
64
|
+
appId: string;
|
|
65
|
+
appName: string | null;
|
|
66
|
+
/** True for first-party/trusted apps (consent always skipped). */
|
|
67
|
+
trusted: boolean;
|
|
68
|
+
alreadyGranted: boolean;
|
|
69
|
+
/** True if the Overworld must show a consent screen before minting a code. */
|
|
70
|
+
consentRequired: boolean;
|
|
71
|
+
}
|
|
72
|
+
export interface AppAuthorizationGrant {
|
|
73
|
+
grantId: string;
|
|
74
|
+
appId: string;
|
|
75
|
+
appName: string | null;
|
|
76
|
+
scopes: string[];
|
|
77
|
+
status: string;
|
|
78
|
+
grantedAt: string;
|
|
79
|
+
revokedAt: string | null;
|
|
80
|
+
}
|
|
81
|
+
/** Thrown by {@link PortalAPI.handleAuthorizeRequest} when the user must consent. */
|
|
82
|
+
export declare class PortalConsentRequiredError extends Error {
|
|
83
|
+
readonly appId: string;
|
|
84
|
+
readonly appName: string | null;
|
|
85
|
+
constructor(appId: string, appName: string | null);
|
|
86
|
+
}
|
|
63
87
|
export interface BeginEntryParams {
|
|
64
88
|
/** Target app id (decimal string). */
|
|
65
89
|
appId: string;
|
|
@@ -105,6 +129,21 @@ export declare class PortalAPI {
|
|
|
105
129
|
* through the Overworld. Requires the current app token on this session.
|
|
106
130
|
*/
|
|
107
131
|
refresh(): Promise<AppTokenResponse>;
|
|
132
|
+
/** Whether portaling into an app needs a consent prompt (Overworld side). */
|
|
133
|
+
getConsent(appId: string): Promise<PortalConsentState>;
|
|
134
|
+
/** Record the user's consent for an app (call from the consent screen). */
|
|
135
|
+
authorizeApp(appId: string, scopes?: string[]): Promise<AppAuthorizationGrant>;
|
|
136
|
+
/** Revoke a prior authorization; also revokes the user's live tokens for it. */
|
|
137
|
+
revokeAppAuthorization(appId: string): Promise<boolean>;
|
|
138
|
+
/** The user's active app authorizations ("connected apps"). */
|
|
139
|
+
myAuthorizedApps(): Promise<AppAuthorizationGrant[]>;
|
|
140
|
+
/** Register/update an app's portal client settings (requires manage_apps). */
|
|
141
|
+
setAppClientSettings(input: {
|
|
142
|
+
appId: string;
|
|
143
|
+
redirectUris?: string[];
|
|
144
|
+
clientType?: string;
|
|
145
|
+
launchUrl?: string;
|
|
146
|
+
}): Promise<PortalConsentState>;
|
|
108
147
|
/**
|
|
109
148
|
* Destination-game side, step 1: generate a PKCE pair, persist the verifier,
|
|
110
149
|
* and return the Overworld authorize URL to navigate to. The caller does
|
|
@@ -116,7 +155,10 @@ export declare class PortalAPI {
|
|
|
116
155
|
* game's params from the URL, mints a code with the session token, and returns
|
|
117
156
|
* the URL to redirect the player back to (carrying `code` + `state`).
|
|
118
157
|
*/
|
|
119
|
-
handleAuthorizeRequest(search?: string
|
|
158
|
+
handleAuthorizeRequest(search?: string, options?: {
|
|
159
|
+
grantConsent?: boolean;
|
|
160
|
+
scopes?: string[];
|
|
161
|
+
}): Promise<string>;
|
|
120
162
|
/**
|
|
121
163
|
* Destination-game side, step 3: read `code` + `state` from the callback URL,
|
|
122
164
|
* load the stored verifier, exchange for an app token, and store it. Returns
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"portal.d.ts","sourceRoot":"","sources":["../../src/domains/portal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAGlD,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,SAAS,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,sDAAsD;IACtD,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,sDAAsD;IACtD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,8EAA8E;AAC9E,MAAM,WAAW,SAAS;IACxB,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC3D,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7C;AAED,iFAAiF;AACjF,qBAAa,uBAAwB,YAAW,SAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,SAAmB;IACtD,OAAO,CAAC,EAAE;IAGV,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAGjC,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAG1C,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;CAG5B;AAqCD,MAAM,WAAW,gBAAgB;IAC/B,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,sGAAsG;IACtG,YAAY,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,WAAW,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,SAAS;IAElB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAFT,UAAU,EAAE,aAAa,EACzB,OAAO,EAAE,YAAY,EACrB,SAAS,GAAE,SAAyC;IAGvE;;;;;OAKG;IACG,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAO5D;;;;OAIG;IACG,uBAAuB,CAAC,MAAM,EAAE;QACpC,KAAK,EAAE,MAAM,CAAC;QACd,aAAa,EAAE,MAAM,CAAC;QACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAC7B,WAAW,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAQpC;;;;OAIG;IACG,YAAY,CAChB,IAAI,EAAE,MAAM,EACZ,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,gBAAgB,CAAC;IAQ5B;;;;OAIG;IACG,OAAO,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAQ1C;;;;OAIG;IACG,UAAU,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC;IAa3D;;;;OAIG;IACG,sBAAsB,CAAC,MAAM,CAAC,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"portal.d.ts","sourceRoot":"","sources":["../../src/domains/portal.ts"],"names":[],"mappings":"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"}
|
package/dist/domains/portal.js
CHANGED
|
@@ -48,6 +48,20 @@ const MintAppTokenDocument = parse(`mutation MintAppToken($input: MintAppTokenIn
|
|
|
48
48
|
const CreatePortalAuthorizationCodeDocument = parse(`mutation CreatePortalAuthorizationCode($input: CreatePortalAuthorizationCodeInput!) { createPortalAuthorizationCode(input: $input) { code redirectUri expiresAt } }`);
|
|
49
49
|
const ExchangePortalCodeDocument = parse(`mutation ExchangePortalCode($input: ExchangePortalCodeInput!) { exchangePortalCode(input: $input) { ${APP_TOKEN_FIELDS} } }`);
|
|
50
50
|
const RefreshAppTokenDocument = parse(`mutation RefreshAppToken { refreshAppToken { ${APP_TOKEN_FIELDS} } }`);
|
|
51
|
+
/** Thrown by {@link PortalAPI.handleAuthorizeRequest} when the user must consent. */
|
|
52
|
+
export class PortalConsentRequiredError extends Error {
|
|
53
|
+
constructor(appId, appName) {
|
|
54
|
+
super(`Consent required for app ${appId}`);
|
|
55
|
+
this.appId = appId;
|
|
56
|
+
this.appName = appName;
|
|
57
|
+
this.name = 'PortalConsentRequiredError';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const PortalConsentDocument = parse(`query PortalConsent($appId: BigInt!) { portalConsent(appId: $appId) { appId appName trusted alreadyGranted consentRequired } }`);
|
|
61
|
+
const AuthorizeAppDocument = parse(`mutation AuthorizeApp($input: AuthorizeAppInput!) { authorizeApp(input: $input) { grantId appId status scopes } }`);
|
|
62
|
+
const RevokeAppAuthorizationDocument = parse(`mutation RevokeAppAuthorization($appId: BigInt!) { revokeAppAuthorization(appId: $appId) }`);
|
|
63
|
+
const MyAuthorizedAppsDocument = parse(`query MyAuthorizedApps { myAuthorizedApps { grantId appId appName scopes status grantedAt revokedAt } }`);
|
|
64
|
+
const SetAppClientSettingsDocument = parse(`mutation SetAppClientSettings($input: SetAppClientSettingsInput!) { setAppClientSettings(input: $input) { appId appName trusted consentRequired } }`);
|
|
51
65
|
export class PortalAPI {
|
|
52
66
|
constructor(management, session, pkceStore = new BrowserSessionPkceStore()) {
|
|
53
67
|
this.management = management;
|
|
@@ -97,6 +111,38 @@ export class PortalAPI {
|
|
|
97
111
|
this.session.setToken(data.refreshAppToken.token);
|
|
98
112
|
return data.refreshAppToken;
|
|
99
113
|
}
|
|
114
|
+
// ----- Consent + connected apps ------------------------------------------
|
|
115
|
+
/** Whether portaling into an app needs a consent prompt (Overworld side). */
|
|
116
|
+
async getConsent(appId) {
|
|
117
|
+
const data = await this.management.request(PortalConsentDocument, { appId });
|
|
118
|
+
return data.portalConsent;
|
|
119
|
+
}
|
|
120
|
+
/** Record the user's consent for an app (call from the consent screen). */
|
|
121
|
+
async authorizeApp(appId, scopes) {
|
|
122
|
+
const data = await this.management.request(AuthorizeAppDocument, {
|
|
123
|
+
input: { appId, scopes },
|
|
124
|
+
});
|
|
125
|
+
return data.authorizeApp;
|
|
126
|
+
}
|
|
127
|
+
/** Revoke a prior authorization; also revokes the user's live tokens for it. */
|
|
128
|
+
async revokeAppAuthorization(appId) {
|
|
129
|
+
const data = await this.management.request(RevokeAppAuthorizationDocument, {
|
|
130
|
+
appId,
|
|
131
|
+
});
|
|
132
|
+
return data.revokeAppAuthorization;
|
|
133
|
+
}
|
|
134
|
+
/** The user's active app authorizations ("connected apps"). */
|
|
135
|
+
async myAuthorizedApps() {
|
|
136
|
+
const data = await this.management.request(MyAuthorizedAppsDocument);
|
|
137
|
+
return data.myAuthorizedApps;
|
|
138
|
+
}
|
|
139
|
+
/** Register/update an app's portal client settings (requires manage_apps). */
|
|
140
|
+
async setAppClientSettings(input) {
|
|
141
|
+
const data = await this.management.request(SetAppClientSettingsDocument, {
|
|
142
|
+
input,
|
|
143
|
+
});
|
|
144
|
+
return data.setAppClientSettings;
|
|
145
|
+
}
|
|
100
146
|
// ----- Browser PKCE redirect helpers -------------------------------------
|
|
101
147
|
/**
|
|
102
148
|
* Destination-game side, step 1: generate a PKCE pair, persist the verifier,
|
|
@@ -120,7 +166,7 @@ export class PortalAPI {
|
|
|
120
166
|
* game's params from the URL, mints a code with the session token, and returns
|
|
121
167
|
* the URL to redirect the player back to (carrying `code` + `state`).
|
|
122
168
|
*/
|
|
123
|
-
async handleAuthorizeRequest(search) {
|
|
169
|
+
async handleAuthorizeRequest(search, options) {
|
|
124
170
|
const params = new URLSearchParams(search ?? defaultSearch());
|
|
125
171
|
const appId = params.get('app_id');
|
|
126
172
|
const codeChallenge = params.get('code_challenge');
|
|
@@ -130,6 +176,18 @@ export class PortalAPI {
|
|
|
130
176
|
if (!appId || !codeChallenge || !redirectUri) {
|
|
131
177
|
throw new Error('authorize request missing app_id, code_challenge, or redirect_uri');
|
|
132
178
|
}
|
|
179
|
+
// Consent gate: trusted apps + already-granted apps proceed silently. For an
|
|
180
|
+
// untrusted, not-yet-granted app, either record consent (when the user
|
|
181
|
+
// approved on the consent screen) or signal the caller to show one.
|
|
182
|
+
const consent = await this.getConsent(appId);
|
|
183
|
+
if (consent.consentRequired) {
|
|
184
|
+
if (options?.grantConsent) {
|
|
185
|
+
await this.authorizeApp(appId, options.scopes);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
throw new PortalConsentRequiredError(appId, consent.appName);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
133
191
|
const result = await this.createAuthorizationCode({
|
|
134
192
|
appId,
|
|
135
193
|
codeChallenge,
|