@wral/studio.mods.auth 0.3.7 → 1.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.
Files changed (45) hide show
  1. package/README.md +39 -47
  2. package/bitbucket-pipelines.yml +25 -1
  3. package/dist/auth.cjs.js +326 -1467
  4. package/dist/auth.es.js +1081 -3093
  5. package/dist/lib.cjs.js +1 -1
  6. package/dist/lib.es.js +13 -7
  7. package/eslint.config.mjs +41 -34
  8. package/index.html +83 -18
  9. package/jest.config.mjs +24 -0
  10. package/jest.setup.mjs +5 -0
  11. package/package.json +15 -28
  12. package/src/auth.mjs +204 -69
  13. package/src/auth.test.mjs +97 -0
  14. package/src/components/auth-app.mjs +26 -0
  15. package/src/components/forgot-password-form.mjs +217 -0
  16. package/src/components/login-form.mjs +288 -0
  17. package/src/config.mjs +27 -0
  18. package/src/helper.mjs +31 -0
  19. package/src/helper.test.mjs +44 -0
  20. package/src/index.mjs +17 -0
  21. package/src/login-layout.mjs +32 -0
  22. package/src/login.mjs +20 -0
  23. package/src/routes/change-password.mjs +158 -0
  24. package/src/routes/dashboard.mjs +17 -0
  25. package/src/routes/index.mjs +15 -0
  26. package/src/state.mjs +61 -0
  27. package/src/state.test.mjs +58 -0
  28. package/src/styles.mjs +9 -0
  29. package/src/token.mjs +40 -0
  30. package/src/utils.mjs +3 -0
  31. package/vellum-fixture.mjs +86 -0
  32. package/vite.config.mjs +12 -0
  33. package/components.html +0 -43
  34. package/development.md +0 -41
  35. package/src/components/mod-auth-login-form.mjs +0 -133
  36. package/src/components/studio-change-password.mjs +0 -84
  37. package/src/components/studio-login.mjs +0 -94
  38. package/src/components/studio-profile-view.mjs +0 -56
  39. package/src/components/studio-reset-password.mjs +0 -110
  40. package/src/lib.mjs +0 -16
  41. package/src/tool-dummy.mjs +0 -84
  42. package/src/util.mjs +0 -194
  43. package/src/util.test.mjs +0 -171
  44. package/vite.config.js +0 -12
  45. package/web-test-runner.config.mjs +0 -28
@@ -0,0 +1,97 @@
1
+ import {
2
+ makeAuth,
3
+ tokenRequestHandler,
4
+ } from './auth.mjs';
5
+ import { JSDOM } from 'jsdom';
6
+ import { jest } from '@jest/globals';
7
+
8
+ function mockToolkit() {
9
+ const dom = new JSDOM(`<!DOCTYPE html><body><studio-app>
10
+ <studio-mod id="mod-auth"></studio-mod>
11
+ </studio-app></body>`);
12
+ return {
13
+ element: dom.window.document.querySelector('studio-app'),
14
+ mod: {
15
+ element: dom.window.document.querySelector('#mod-auth'),
16
+ },
17
+ dispatchAction: jest.fn(),
18
+ };
19
+ }
20
+
21
+ describe('auth mod', () => {
22
+
23
+ describe('makeAuth', () => {
24
+
25
+ test('should return an object with method-like properties', () => {
26
+ const mod = makeAuth({
27
+ toolkit: mockToolkit(),
28
+ });
29
+ expect(mod).toHaveProperty('mount');
30
+ expect(mod).toHaveProperty('handleTokenRequest');
31
+ expect(mod).toHaveProperty('handleDestroyAuth');
32
+ expect(mod).toHaveProperty('presentLoginForm');
33
+ expect(mod).toHaveProperty('getToken');
34
+ expect(mod).toHaveProperty('saveToken');
35
+ expect(mod).toHaveProperty('getFreshToken');
36
+ expect(mod).toHaveProperty('requestRender');
37
+ });
38
+
39
+ });
40
+
41
+ describe('mount', () => {
42
+ test('registers action handlers and layout', () => {
43
+ const mod = makeAuth({
44
+ toolkit: mockToolkit(),
45
+ });
46
+ mod.mount();
47
+ expect(mod.config.toolkit.dispatchAction).toHaveBeenCalledWith({
48
+ type: 'action:register',
49
+ detail: {
50
+ actionType: 'auth:requestToken',
51
+ handler: expect.any(Function),
52
+ modName: 'auth',
53
+ },
54
+ });
55
+ expect(mod.config.toolkit.dispatchAction).toHaveBeenCalledWith({
56
+ type: 'action:register',
57
+ detail: {
58
+ actionType: 'auth:destroy',
59
+ handler: expect.any(Function),
60
+ modName: 'auth',
61
+ },
62
+ });
63
+ expect(mod.config.toolkit.dispatchAction).toHaveBeenCalledWith({
64
+ type: 'layout:register',
65
+ detail: {
66
+ name: 'auth:login',
67
+ slots: ['main'],
68
+ templateFn: expect.any(Function),
69
+ },
70
+ });
71
+ });
72
+ });
73
+
74
+ describe('tokenRequestHandler', () => {
75
+ test('returns a handler that resolves to a fresh token', async () => {
76
+ const handler = tokenRequestHandler({
77
+ getFreshToken: jest.fn(async () => 'token'),
78
+ });
79
+ const callback = jest.fn();
80
+ await handler({ callback });
81
+ expect(callback).toHaveBeenCalledWith('token');
82
+ });
83
+ test('presents login form if getFreshToken fails', async () => {
84
+ const presentLoginForm = jest.fn();
85
+ const handler = tokenRequestHandler({
86
+ log: jest.fn(),
87
+ getFreshToken: jest.fn().mockRejectedValue(new Error('fail')),
88
+ presentLoginForm,
89
+ });
90
+ const callback = jest.fn();
91
+ await handler({ callback });
92
+ expect(callback).not.toHaveBeenCalled();
93
+ expect(presentLoginForm).toHaveBeenCalled();
94
+ });
95
+ });
96
+
97
+ });
@@ -0,0 +1,26 @@
1
+ import { LitElement } from 'lit';
2
+ import { renderRoute } from '../routes/index.mjs';
3
+ import appStyles from '../styles.mjs';
4
+
5
+ class AuthApp extends LitElement {
6
+ static get styles() {
7
+ return [appStyles];
8
+ }
9
+ static get properties() {
10
+ return {
11
+ state: { type: Object },
12
+ auth: { type: Object },
13
+ };
14
+ }
15
+
16
+ render() {
17
+ const { state, auth } = this;
18
+ return renderRoute(state, auth);
19
+ }
20
+ }
21
+
22
+ export default AuthApp;
23
+
24
+ if (!customElements.get('auth-app')) {
25
+ customElements.define('auth-app', AuthApp);
26
+ }
@@ -0,0 +1,217 @@
1
+ import { html, css, LitElement } from 'lit';
2
+ import login from '../login.mjs';
3
+ import { createClient } from '@wral/sdk-auth';
4
+
5
+ const stages = Object.freeze({
6
+ REQUEST: 'request',
7
+ CONFIRM: 'confirm',
8
+ FINISHED: 'finished',
9
+ });
10
+
11
+ export class ForgotPasswordForm extends LitElement {
12
+
13
+ static get properties() {
14
+ return {
15
+ api: { type: String, attribute: 'api' },
16
+ confirmation: { type: 'String', attribute: 'confirmation' },
17
+ };
18
+ }
19
+
20
+ static get styles() {
21
+ return css`
22
+ :host {
23
+ display: block;
24
+ font-family: Arial, sans-serif;
25
+ }
26
+ form input[type="text"],
27
+ form input[type="password"] {
28
+ width: 100%;
29
+ margin: 5px 0 15px 0;
30
+ padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
31
+ box-sizing: border-box;
32
+ border: 1px solid var(--color-gray-300, #ccc);
33
+ border-radius: var(--radius-sm, 5px);
34
+ color: var(--color-gray-9, #333);
35
+ background-color: var(--color-gray-0, transparent);
36
+ }
37
+ form input[type="submit"] {
38
+ background-color:var(--color-primary, inherit);
39
+ color: var(--color-gray-0, #fff);
40
+ border: none;
41
+ padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
42
+ border-radius: var(--radius-dynamic, 5px);
43
+ width: 200px;
44
+ display: block;
45
+ margin: var(--spacing-md, 1rem) auto;
46
+ cursor: pointer;
47
+ }
48
+ form label {
49
+ font-size: 14px;
50
+ color: var(--color-gray-9, #333);
51
+ font-weight: bold;
52
+ margin-bottom: var(--spacing-sm, 0.5rem);
53
+ }
54
+ .error {
55
+ color: var(--color-danger, red);
56
+ font-size: 12px;
57
+ }
58
+ `;
59
+ }
60
+
61
+ constructor() {
62
+ super();
63
+ this.errors = {};
64
+ this.stage = stages.REQUEST;
65
+ }
66
+
67
+ connectedCallback() {
68
+ console.log('[auth]','[forgot-password]', { confirmation: this.confirmation });
69
+ super.connectedCallback();
70
+ // If there is a confirmation, update stage
71
+ if (this.confirmation && this.stage !== stages.FINISHED) {
72
+ this.stage = stages.CONFIRM;
73
+ }
74
+ this.requestUpdate();
75
+ }
76
+
77
+ handleSubmitRequest(e) {
78
+ e.preventDefault();
79
+ // Check for submission errors (none since username is required)
80
+ const username = e.target.username.value;
81
+ this.username = username;
82
+
83
+ // Send API Request
84
+ const authClient = createClient({
85
+ baseUrl: this.api,
86
+ });
87
+ authClient.triggerResetPassword({ username }).then(() => {
88
+ // Update stage
89
+ console.log('[auth]', 'triggered reset password for ', username);
90
+ this.stage = stages.CONFIRM;
91
+ }).catch((error) => {
92
+ console.error('[auth] error triggering reset password', error);
93
+ this.errors['general'] = error.message ||
94
+ 'Unable to send password reset request. Please try again later.';
95
+ }).finally(() => {
96
+ this.requestUpdate();
97
+ });
98
+
99
+ }
100
+
101
+ handleSubmitConfirm(e) {
102
+ e.preventDefault();
103
+
104
+ // Check for submission errors (passwords match)
105
+ const password = e.target.password.value;
106
+ const confirmPassword = e.target['confirm-password'].value;
107
+ if (password !== confirmPassword) {
108
+ this.errors['confirm-password'] = 'Passwords do not match.';
109
+ this.requestUpdate();
110
+ return;
111
+ }
112
+ const confirmationCode = this.confirmation || e.target['confirmation'].value;
113
+ if (!confirmationCode) {
114
+ this.errors['general'] = 'Confirmation code is required.';
115
+ this.requestUpdate();
116
+ return;
117
+ }
118
+
119
+ // Send API request
120
+ const authClient = createClient({
121
+ baseUrl: this.api,
122
+ });
123
+ authClient.confirmResetPassword({
124
+ username: this.username,
125
+ newPassword: password,
126
+ confirmationCode,
127
+ }).then(() => {
128
+ // Login and emit a login event
129
+ // TODO: DRY this login event
130
+ return login(this, { username: this.username, password }).then(({token}) => {
131
+ // emit a success event
132
+ this.dispatchEvent(new CustomEvent('login-success', {
133
+ detail: { token },
134
+ composed: true,
135
+ bubbles: true,
136
+ }));
137
+ });
138
+ }).catch((error) => {
139
+ console.error('[auth][forgot-password-form]',error);
140
+ this.errors['general'] = 'Unable to reset password. Please try again later.';
141
+ }).finally(() => {
142
+ this.requestUpdate();
143
+ });
144
+ }
145
+
146
+ render() {
147
+ switch (this.stage) {
148
+ case stages.CONFIRM:
149
+ return this.renderConfirmForm();
150
+ case stages.FINISHED:
151
+ return html`<p>Your password has been reset.</p>`;
152
+ case stages.REQUEST_SENT:
153
+ return html`<p>A email has been sent with instructions to finish
154
+ resetting your password.</p>`;
155
+ case stages.REQUEST:
156
+ default:
157
+ return this.renderRequestForm();
158
+ }
159
+ }
160
+
161
+ renderRequestForm() {
162
+ return html`<form @submit=${this.handleSubmitRequest}>
163
+ <label for="username">Username/Email</label>
164
+ <input
165
+ type="text"
166
+ name="username"
167
+ placeholder="Email or Username"
168
+ required
169
+ />
170
+ <p class="error">${this.errors['general']}</p>
171
+ <input type="submit" value="Request password reset" />
172
+ </form>`;
173
+ }
174
+
175
+ renderConfirmForm() {
176
+ return html`<form @submit=${this.handleSubmitConfirm}>
177
+ <p>You should have received a confirmation code to reset your password</p>
178
+
179
+ ${this.confirmation ? ''
180
+ : html`<label for="confirmation">Confirmation Code</label>
181
+ <input
182
+ type="text"
183
+ name="confirmation"
184
+ placeholder="Confirmation Code"
185
+ required
186
+ />`}
187
+
188
+ <label for="password">New Password</label>
189
+ <input
190
+ type="password"
191
+ name="password"
192
+ placeholder="Password"
193
+ required
194
+ />
195
+ ${this.errors['password']
196
+ ? html`<p class="error">${this.errors['password']}</p>`
197
+ : ''}
198
+
199
+ <label for="confirm-password">Confirm Password</label>
200
+ <input
201
+ type="password"
202
+ name="confirm-password"
203
+ placeholder="Confirm Password"
204
+ required
205
+ />
206
+ ${this.errors['confirm-password']
207
+ ? html`<p class="error">${this.errors['confirm-password']}</p>`
208
+ : ''}
209
+
210
+ ${this.errors['general']
211
+ ? html`<p class="error">${this.errors['general']}</p>`
212
+ : ''}
213
+ <input type="submit" value="Reset Password" />
214
+ </form>`;
215
+ }
216
+
217
+ }
@@ -0,0 +1,288 @@
1
+ /**
2
+ * This login form is not part of any application route, because it represents
3
+ * the public facing unauthenticated side of the application.
4
+ *
5
+ * Anything regarding login that is handled by an unauthenticated user should
6
+ * be handled through here.
7
+ *
8
+ * Other routes assume the user is authenticated.
9
+ */
10
+ import { LitElement, html, css } from 'lit';
11
+ import login from '../login.mjs';
12
+ import { ForgotPasswordForm } from './forgot-password-form.mjs';
13
+ import { createClient } from '@wral/sdk-auth';
14
+
15
+
16
+ class LoginForm extends LitElement {
17
+
18
+ static get properties() {
19
+ return {
20
+ api: { type: String, attribute: 'api' },
21
+ };
22
+ }
23
+
24
+ static get styles() {
25
+ return css`
26
+ :host {
27
+ display: block;
28
+ font-family: Arial, sans-serif;
29
+ }
30
+ .login-form {
31
+ background-color: var(--color-gray-0, #fff);
32
+ padding: var(--spacing-xl, 3rem);
33
+ border-radius: var(--radius-md, 5px);
34
+ box-shadow: var(--drop-shadow-md, none);
35
+ }
36
+ form input[type="text"],
37
+ form input[type="password"] {
38
+ width: 100%;
39
+ margin: 5px 0 15px 0;
40
+ padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
41
+ box-sizing: border-box;
42
+ border: 1px solid var(--color-gray-300, #ccc);
43
+ border-radius: var(--radius-sm, 5px);
44
+ color: var(--color-gray-9, #333);
45
+ background-color: var(--color-gray-0, transparent);
46
+ }
47
+ form input[type="submit"] {
48
+ background-color: var(--color-primary, inherit);
49
+ color: var(--color-gray-0, #fff);
50
+ border: none;
51
+ padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
52
+ border-radius: var(--radius-dynamic, 5px);
53
+ width: 200px;
54
+ display: block;
55
+ margin: var(--spacing-md, 1rem) auto;
56
+ cursor: pointer;
57
+ }
58
+ form label {
59
+ font-size: 14px;
60
+ color: var(--color-gray-9, #333);
61
+ font-weight: bold;
62
+ margin-bottom: var(--spacing-sm, 0.5rem);
63
+ }
64
+ a.forgot {
65
+ color: var(--color-primary, inherit);
66
+ text-decoration: none;
67
+ font-size: 12px;
68
+ font-weight: bold;
69
+ }
70
+ .error {
71
+ color: var(--color-error, red);
72
+ font-size: 12px;
73
+ margin-top: var(--spacing-sm, 0.5rem);
74
+ }
75
+ .header {
76
+ display: flex;
77
+ flex-direction: column;
78
+ align-items: center;
79
+ }
80
+ .header .intro {
81
+ width: 200px;
82
+ margin-bottom: 0;
83
+ color: var(--color-gray-3);
84
+ }
85
+ .header .brand {
86
+ font-size: 32px;
87
+ min-width: 200px;
88
+ margin-bottom: var(--spacing-lg, 2rem);
89
+ }
90
+ .header .brand em {
91
+ text-transform: uppercase;
92
+ font-weight: bold;
93
+ font-style: normal;
94
+ }
95
+ `;
96
+ }
97
+
98
+ static get states() {
99
+ return {
100
+ DEFAULT: 'DEFAULT',
101
+ FORGOT: 'FORGOT',
102
+ CHALLENGE_CHANGE_PW: 'CHALLENGE_CHANGE_PW',
103
+ };
104
+ }
105
+
106
+ constructor() {
107
+ super();
108
+ this.formState = LoginForm.states.DEFAULT;
109
+ this.username = '';
110
+ this.password = '';
111
+ this.token = undefined;
112
+ this.errors = [];
113
+ }
114
+
115
+ onSubmit(e) {
116
+ e.preventDefault();
117
+ const formElem = e.explicitOriginalTarget.parentElement;
118
+ console.log(formElem);
119
+ const formData = new FormData(formElem).entries().reduce((acc, [key, value]) => {
120
+ acc[key] = value;
121
+ return acc;
122
+ }, {});
123
+
124
+ login(this, formData).then(({token, challenge}) => {
125
+ if (challenge && challenge.name === 'NEW_PASSWORD_REQUIRED') {
126
+
127
+ // store these to use in later forms
128
+ this.token = token;
129
+ this.username = formData['username'];
130
+ this.password = formData['password'];
131
+
132
+ this.setState(LoginForm.states.CHALLENGE_CHANGE_PW);
133
+ // don't dispatch a success event with the token
134
+ // The user should change their password first
135
+ return;
136
+ }
137
+
138
+ // emit a success event
139
+ this.dispatchEvent(new CustomEvent('login-success', {
140
+ detail: { token },
141
+ }));
142
+ }).catch((error) => {
143
+ console.log(error);
144
+ this.errors = [error.message];
145
+ this.requestUpdate();
146
+ });
147
+ }
148
+
149
+ onSubmitChangePasswordChallenge(e) {
150
+ e.preventDefault();
151
+ const formElem = e.explicitOriginalTarget;
152
+ const formData = new FormData(formElem).entries().reduce((acc, [key, value]) => {
153
+ acc[key] = value;
154
+ return acc;
155
+ }, {});
156
+
157
+ // validate password
158
+ const password = formData['password'];
159
+ const confirmPassword = formData['confirm-password'];
160
+ if (password !== confirmPassword) {
161
+ this.errors = ['Passwords do not match.'];
162
+ this.requestUpdate();
163
+ return;
164
+ }
165
+
166
+ const authClient = createClient({
167
+ baseUrl: this.api,
168
+ });
169
+ authClient.updatePassword({
170
+ token: this.token,
171
+ currentPassword: this.password,
172
+ newPassword: password,
173
+ }).then(() => {
174
+ // emit a success event
175
+ this.dispatchEvent(new CustomEvent('login-success', {
176
+ detail: { token: this.token },
177
+ }));
178
+ }).catch((error) => {
179
+ console.log(error);
180
+ this.errors = [error.message];
181
+ this.requestUpdate();
182
+ });
183
+ }
184
+
185
+ setState(newState) {
186
+ if (newState !== this.formState) {
187
+ this.formState = newState;
188
+ this.errors = [];
189
+ this.requestUpdate();
190
+ }
191
+ }
192
+
193
+ render() {
194
+ return html`<div class="login-form">
195
+ ${this.renderHeader()}
196
+ ${this.formState === LoginForm.states.FORGOT
197
+ ? this.renderForgotPasswordForm() : ''}
198
+ ${this.formState === LoginForm.states.CHALLENGE_CHANGE_PW
199
+ ? this.renderChangePasswordForm() : ''}
200
+ ${this.formState === LoginForm.states.DEFAULT
201
+ ? this.renderLoginForm() : ''}
202
+ </div>`;
203
+ }
204
+
205
+ /**
206
+ * TODO: accept the header as a slot
207
+ */
208
+ renderHeader() {
209
+ return html`<div class="header">
210
+ <div class="intro">
211
+ Welcome to
212
+ </div>
213
+ <div class="brand">
214
+ <em>WRAL</em>.studio
215
+ </div>
216
+ </div>`;
217
+ }
218
+
219
+ renderLoginForm() {
220
+ return html`
221
+ <form>
222
+ <label for="username">User</label>
223
+ <input
224
+ type="text"
225
+ name="username"
226
+ placeholder="Email or Username"
227
+ required
228
+ />
229
+
230
+ <label for="password">Password</label>
231
+ <input
232
+ type="password"
233
+ name="password"
234
+ placeholder="Password"
235
+ required
236
+ />
237
+ <div>
238
+ <a href="#" class="forgot"
239
+ @click=${() => {this.setState(LoginForm.states.FORGOT);}}
240
+ >Forgot password</a>
241
+ </div>
242
+
243
+ ${this.errors
244
+ ? this.errors.map(error => html`<p class="error">${error}</p>`)
245
+ : ''}
246
+ <input type="submit" value="Log in" @click=${this.onSubmit} />
247
+
248
+ </form>`;
249
+ }
250
+
251
+ renderForgotPasswordForm() {
252
+ return html`
253
+ <auth-forgot-password-form
254
+ api=${this.api}>
255
+ </auth-forgot-password-form>
256
+ `;
257
+ }
258
+
259
+ renderChangePasswordForm() {
260
+ return html`
261
+ <p>You have logged in successfully, but you must change your password.</p>
262
+ <form @submit=${this.onSubmitChangePasswordChallenge}>
263
+ <input type="hidden" name="username" value="${this.username}" />
264
+ <label for="password">New Password</label>
265
+ <input
266
+ type="password"
267
+ name="password"
268
+ placeholder="Password"
269
+ required
270
+ />
271
+ <label for="confirm-password">Confirm Password</label>
272
+ <input
273
+ type="password"
274
+ name="confirm-password"
275
+ placeholder="Confirm Password"
276
+ required
277
+ />
278
+ ${this.errors
279
+ ? this.errors.map(error => html`<p class="error">${error}</p>`)
280
+ : ''}
281
+ <input type="submit" value="Change Password" />
282
+ </form>
283
+ `;
284
+ }
285
+ }
286
+
287
+ customElements.define('auth-login-form', LoginForm);
288
+ customElements.define('auth-forgot-password-form', ForgotPasswordForm);
package/src/config.mjs ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Gets configuration for the Auth Mod from the attributes
3
+ * of the mod element (in DOM).
4
+ * @param {VellumToolkit} toolkit
5
+ * @returns {Object}
6
+ */
7
+ export function getConfig(toolkit, mod) {
8
+ const modElement = mod.element;
9
+ const attrs = modElement.attributes;
10
+ console.log(attrs);
11
+ return {
12
+ api: attrs.api.value || requiredConfig('auth:api is required'),
13
+ forceLogin: attrs['force-login'] !== undefined, // TODO: handle false
14
+ storageKey: attrs['storage-key']?.value,
15
+ mount: attrs['path']?.value || '/auth',
16
+ toolkit,
17
+ };
18
+ }
19
+
20
+ /**
21
+ * Helper for required config elements
22
+ * This defines what to do when a required config is missing
23
+ * @param {string} message
24
+ */
25
+ function requiredConfig(message) {
26
+ throw new Error(message);
27
+ }
package/src/helper.mjs ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * This is a helper for consumers of the Auth mod to get
3
+ * an API token.
4
+ * @example <code>
5
+ * async () => {
6
+ * const myElem = document.querySelector('my-element');
7
+ * const token = await getToken(myElem);
8
+ * someApiCall({ token, ... });
9
+ * }
10
+ * </code>
11
+ * @param {Element} eventTarget - a DOM element that will trigger the event
12
+ * @returns {Promise} - a promise that resolves with the token
13
+ */
14
+ export function getToken(eventTarget) {
15
+ return new Promise((resolve, reject) => {
16
+ if (!eventTarget || !eventTarget.dispatchEvent) {
17
+ reject(new Error('getToken must be called with a DOM element'));
18
+ }
19
+ eventTarget.dispatchEvent(new CustomEvent('harness:action', {
20
+ detail: {
21
+ type: 'auth:requestToken',
22
+ detail: {
23
+ callback: resolve,
24
+ },
25
+ },
26
+ bubbles: true,
27
+ composed: true,
28
+ cancelable: true,
29
+ }));
30
+ });
31
+ }