@aerogel/plugin-solid 0.0.1 → 0.1.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 (39) hide show
  1. package/dist/InruptAuthenticator.lazy-BvpQLGoj.js +9 -0
  2. package/dist/InruptAuthenticator.lazy-BvpQLGoj.js.map +1 -0
  3. package/dist/LegacyAuthenticator.lazy-ChFFqP14.js +5 -0
  4. package/dist/LegacyAuthenticator.lazy-ChFFqP14.js.map +1 -0
  5. package/dist/aerogel-plugin-solid.d.ts +120 -92
  6. package/dist/aerogel-plugin-solid.js +724 -0
  7. package/dist/aerogel-plugin-solid.js.map +1 -0
  8. package/package.json +35 -28
  9. package/src/auth/Authenticator.ts +10 -8
  10. package/src/auth/authenticators/InruptAuthenticator.lazy.ts +1 -0
  11. package/src/auth/authenticators/InruptAuthenticator.ts +41 -12
  12. package/src/auth/authenticators/LegacyAuthenticator.lazy.ts +1 -0
  13. package/src/auth/authenticators/LegacyAuthenticator.ts +80 -0
  14. package/src/auth/index.ts +3 -1
  15. package/src/components/SolidAvatar.vue +35 -0
  16. package/src/components/SolidImage.vue +33 -0
  17. package/src/components/SolidLogin.vue +141 -0
  18. package/src/components/index.ts +3 -4
  19. package/src/errors/AuthenticationFailedError.ts +22 -0
  20. package/src/errors/index.ts +3 -4
  21. package/src/forms/validation.ts +11 -0
  22. package/src/index.css +1 -0
  23. package/src/index.test.ts +24 -0
  24. package/src/index.ts +65 -0
  25. package/src/services/Solid.state.ts +12 -16
  26. package/src/services/Solid.ts +317 -139
  27. package/src/testing/setup.ts +11 -0
  28. package/src/types/env.d.ts +1 -0
  29. package/src/types/solid-auth-client.d.ts +21 -0
  30. package/.eslintrc.js +0 -7
  31. package/dist/aerogel-plugin-solid.cjs.js +0 -2
  32. package/dist/aerogel-plugin-solid.cjs.js.map +0 -1
  33. package/dist/aerogel-plugin-solid.esm.js +0 -2
  34. package/dist/aerogel-plugin-solid.esm.js.map +0 -1
  35. package/noeldemartin.config.js +0 -2
  36. package/src/components/AGSolidStatus.ts +0 -5
  37. package/src/components/AGSolidStatus.vue +0 -60
  38. package/src/main.ts +0 -43
  39. package/tsconfig.json +0 -10
@@ -1,39 +1,69 @@
1
1
  import {
2
- PromisedValue,
2
+ after,
3
3
  arrayFilter,
4
4
  arrayUnique,
5
5
  arrayWithout,
6
+ asyncMemo,
6
7
  facade,
7
8
  fail,
8
9
  getLocationQueryParameter,
9
10
  hasLocationQueryParameter,
11
+ isDevelopment,
10
12
  objectWithout,
13
+ objectWithoutEmpty,
11
14
  parseBoolean,
15
+ requireUrlParse,
16
+ resetAsyncMemo,
17
+ setAsyncMemo,
12
18
  tap,
13
19
  urlRoot,
14
20
  } from '@noeldemartin/utils';
21
+ import { App, Errors, Events, Storage, UI, translateWithDefault } from '@aerogel/core';
15
22
  import { fetchLoginUserProfile } from '@noeldemartin/solid-utils';
16
- import { App, Errors, Events, UI, translateWithDefault } from '@aerogel/core';
23
+ import { setEngine } from 'soukai';
24
+ import { SolidACLAuthorization, SolidContainer, SolidDocument, SolidTypeIndex } from 'soukai-solid';
25
+ import type { ErrorSource } from '@aerogel/core';
17
26
  import type { Fetch, SolidModelConstructor } from 'soukai-solid';
18
- import { SolidACLAuthorization, SolidContainer, SolidTypeIndex } from 'soukai-solid';
19
- import type { SolidUserProfile } from '@noeldemartin/solid-utils';
20
-
21
- import AuthenticationCancelledError from '@/errors/AuthenticationCancelledError';
22
- import { ContainerAlreadyInUse } from '@/errors';
23
- import { getAuthenticator } from '@/auth';
24
- import type { AuthenticatorName } from '@/auth';
25
- import type Authenticator from '@/auth/Authenticator';
26
- import type { AuthSession } from '@/auth/Authenticator';
27
-
28
- import Service from './Solid.state';
27
+ import type { NullablePartial } from '@noeldemartin/utils';
28
+ import type { SolidStore, SolidUserProfile } from '@noeldemartin/solid-utils';
29
+
30
+ import AuthenticationCancelledError from '@aerogel/plugin-solid/errors/AuthenticationCancelledError';
31
+ import { ContainerAlreadyInUse } from '@aerogel/plugin-solid/errors';
32
+ import { getAuthenticator } from '@aerogel/plugin-solid/auth';
33
+ import type Authenticator from '@aerogel/plugin-solid/auth/Authenticator';
34
+ import type { AuthenticatorName } from '@aerogel/plugin-solid/auth';
35
+ import type { AuthSession } from '@aerogel/plugin-solid/auth/Authenticator';
36
+
37
+ import Service, { DEFAULT_STATE } from './Solid.state';
38
+
39
+ export type LoginOptions = NullablePartial<{
40
+ authenticator: AuthenticatorName;
41
+ onError(error: ErrorSource): unknown;
42
+ fallbackUrl: string;
43
+ loading: boolean;
44
+ }>;
45
+
46
+ export type UserProfileOptions = NullablePartial<{
47
+ required: boolean;
48
+ markStale: boolean;
49
+ }>;
50
+
51
+ export type ReconnectOptions = Omit<LoginOptions, 'authenticator'> & {
52
+ force?: boolean;
53
+ };
29
54
 
30
55
  export class SolidService extends Service {
31
56
 
32
- protected publicTypeIndexes: Record<string, PromisedValue<SolidTypeIndex>> = {};
33
- protected privateTypeIndexes: Record<string, PromisedValue<SolidTypeIndex>> = {};
34
-
35
57
  public isLoggedIn(): this is { session: AuthSession; user: SolidUserProfile; authenticator: Authenticator } {
36
- return this.loggedIn;
58
+ return !!this.session;
59
+ }
60
+
61
+ public wasLoggedIn(): this is { user: SolidUserProfile } {
62
+ return !!this.previousSession;
63
+ }
64
+
65
+ public hasLoggedIn(): this is { user: SolidUserProfile } {
66
+ return !!(this.session || this.previousSession);
37
67
  }
38
68
 
39
69
  public requireAuthenticator(): Authenticator {
@@ -41,26 +71,49 @@ export class SolidService extends Service {
41
71
  }
42
72
 
43
73
  public requireUser(): SolidUserProfile {
44
- return this.user ?? fail('Could not get user profile');
74
+ return this.user ?? fail('Could not get user profile (are you logged in?)');
45
75
  }
46
76
 
47
77
  public requireUserProfile(url: string): Promise<SolidUserProfile> {
48
- return this.getUserProfile(url, true);
78
+ return this.getUserProfile(url, { required: true });
49
79
  }
50
80
 
51
- public async getUserProfile(url: string, required: true): Promise<SolidUserProfile>;
52
- public async getUserProfile(url: string, required?: false): Promise<SolidUserProfile | null>;
53
- public async getUserProfile(url: string, required: boolean = false): Promise<SolidUserProfile | null> {
54
- return (
81
+ /* eslint-disable max-len */
82
+ public async getUserProfile(url: string, options: Omit<UserProfileOptions, 'required'> & { required: true }): Promise<SolidUserProfile>; // prettier-ignore
83
+ public async getUserProfile(url: string, options?: UserProfileOptions): Promise<SolidUserProfile | null>;
84
+ /* eslint-enable max-len */
85
+
86
+ public async getUserProfile(url: string, options: UserProfileOptions = {}): Promise<SolidUserProfile | null> {
87
+ let profileStore: SolidStore | null = null;
88
+ const markStale = options.markStale ?? !this.authenticator?.getAuthenticatedFetch();
89
+ const userProfile =
55
90
  this.profiles[url] ??
56
- tap(
91
+ (await tap(
57
92
  await fetchLoginUserProfile(url, {
58
93
  fetch: this.fetch,
59
- required,
94
+ required: options.required ?? false,
95
+ onLoaded(store) {
96
+ profileStore = store;
97
+ },
60
98
  }),
61
- (profile) => profile && this.rememberProfile(profile),
62
- )
63
- );
99
+ async (profile) => {
100
+ if (!profile) {
101
+ return;
102
+ }
103
+
104
+ if (profileStore) {
105
+ await Events.emit('solid:user-profile-loaded', [profile, profileStore]);
106
+ }
107
+
108
+ this.rememberProfile(profile);
109
+ },
110
+ ));
111
+
112
+ if (userProfile && markStale) {
113
+ this.setState({ staleProfiles: arrayUnique(this.staleProfiles.concat([userProfile.webId])) });
114
+ }
115
+
116
+ return userProfile;
64
117
  }
65
118
 
66
119
  public async refreshUserProfile(): Promise<void> {
@@ -80,14 +133,17 @@ export class SolidService extends Service {
80
133
  });
81
134
  }
82
135
 
83
- public async login(loginUrl: string, authenticatorName?: AuthenticatorName): Promise<boolean> {
84
- authenticatorName = authenticatorName ?? this.preferredAuthenticator ?? 'default';
136
+ public async login(loginUrl: string, options: LoginOptions = {}): Promise<boolean> {
137
+ if (isDevelopment() && loginUrl === 'dev') {
138
+ loginUrl = 'http://localhost:3000';
139
+ }
85
140
 
86
- if (App.isDevelopment && loginUrl === 'devserver') {
87
- loginUrl = 'http://localhost:4000';
141
+ if (!/^https?:\/\//.test(loginUrl)) {
142
+ options.fallbackUrl ??= `http://${loginUrl}`;
143
+ loginUrl = `https://${loginUrl}`;
88
144
  }
89
145
 
90
- if (this.loggedIn) {
146
+ if (this.isLoggedIn()) {
91
147
  return true;
92
148
  }
93
149
 
@@ -95,59 +151,84 @@ export class SolidService extends Service {
95
151
  throw new Error('Authentication already in progress');
96
152
  }
97
153
 
98
- const staleTimeout = setTimeout(() => (this.loginStale = true), 10000);
99
- this.loginOngoing = true;
100
-
101
- try {
102
- const profile = await this.getUserProfile(loginUrl);
103
- const oidcIssuerUrl = profile?.oidcIssuerUrl ?? urlRoot(profile?.webId ?? loginUrl);
104
- const authenticator = await this.bootAuthenticator(authenticatorName);
105
-
106
- this.setState({
107
- dismissed: false,
108
- ignorePreviousSessionError: true,
109
- previousSession: {
110
- loginUrl,
111
- avatarUrl: profile?.avatarUrl,
112
- authenticator: authenticatorName,
113
- error: translateWithDefault(
114
- 'auth.stuckConnecting',
115
- 'We didn\'t hear back from the identity provider, maybe try reconnecting?',
116
- ),
117
- },
118
- });
119
-
120
- await authenticator.login(oidcIssuerUrl);
121
-
122
- return true;
123
- } catch (error) {
124
- if (error instanceof AuthenticationCancelledError) {
125
- return false;
126
- }
127
-
128
- App.isMounted ? Errors.report(error) : this.setState({ loginError: error });
129
-
130
- return false;
131
- } finally {
132
- clearTimeout(staleTimeout);
133
-
134
- this.loginOngoing = false;
135
- this.loginStale = false;
154
+ if (!options.loading) {
155
+ return this.attemptLogin(loginUrl, options);
136
156
  }
157
+
158
+ return UI.loading(translateWithDefault('solid.loginOngoing', 'Logging in...'), () => {
159
+ return this.attemptLogin(loginUrl, options);
160
+ });
137
161
  }
138
162
 
139
- public async reconnect(force: boolean = false): Promise<void> {
140
- if (this.loggedIn || !this.previousSession || (this.previousSession.error !== null && !force)) {
163
+ public async reconnect(options: ReconnectOptions = {}): Promise<void> {
164
+ const { force, ...loginOptions } = options;
165
+ if (this.isLoggedIn() || !this.previousSession || (this.previousSession.error !== null && !force)) {
141
166
  return;
142
167
  }
143
168
 
144
- await this.login(this.previousSession.loginUrl, this.previousSession.authenticator);
169
+ await this.login(this.previousSession.loginUrl, {
170
+ authenticator: this.previousSession.authenticator,
171
+ ...loginOptions,
172
+ loading: loginOptions.loading ?? false,
173
+ });
145
174
  }
146
175
 
147
176
  public async logout(force: boolean = false): Promise<void> {
148
- const confirmLogout =
149
- force ||
150
- (await UI.confirm(translateWithDefault('solid.logoutConfirm', 'Are you sure you want to log out?')));
177
+ const isCloudReady = !App.plugin('@aerogel/local-first') || !!App.service('$cloud')?.ready;
178
+ const hasLocalChanges = !App.plugin('@aerogel/local-first') || !!App.service('$cloud')?.dirty;
179
+ const [confirmLogout, options] = force
180
+ ? [true, { wipeLocalData: isCloudReady }]
181
+ : await UI.confirm(
182
+ isCloudReady
183
+ ? translateWithDefault('solid.logoutConfirmTitle', 'Log out from this device?')
184
+ : translateWithDefault('solid.disconnectConfirmTitle', 'Disconnect account?'),
185
+ isCloudReady
186
+ ? hasLocalChanges
187
+ ? translateWithDefault(
188
+ 'solid.logoutConfirmMessageWithLocalChanges',
189
+ 'There are some changes that haven\'t been synchronized and will be lost, ' +
190
+ 'but the rest of the data will remain in your Solid POD.',
191
+ )
192
+ : translateWithDefault(
193
+ 'solid.logoutConfirmMessage',
194
+ 'Logging out will remove all the data from this device, ' +
195
+ 'but you\'ll still have it in your Solid POD.',
196
+ )
197
+ : translateWithDefault(
198
+ 'solid.disconnectConfirmMessage',
199
+ 'You\'ll need to introduce your credentials again to connect to your Solid POD.',
200
+ ),
201
+ {
202
+ acceptText: translateWithDefault('solid.logoutConfirmAccept', 'Log out'),
203
+ acceptVariant: 'danger',
204
+ cancelText: translateWithDefault(
205
+ 'solid.logoutConfirmCancel',
206
+ translateWithDefault('ui.cancel', 'Cancel'),
207
+ ),
208
+ checkboxes: isCloudReady
209
+ ? hasLocalChanges
210
+ ? {
211
+ wipeLocalData: {
212
+ label: translateWithDefault(
213
+ 'solid.logoutConfirmWipeNotice',
214
+ 'I understand that changes that haven\'t been synchronized will be lost.',
215
+ ),
216
+ default: false,
217
+ required: true,
218
+ },
219
+ }
220
+ : undefined
221
+ : {
222
+ wipeLocalData: {
223
+ label: translateWithDefault(
224
+ 'solid.logoutConfirmWipe',
225
+ 'Also remove all existing data',
226
+ ),
227
+ default: false,
228
+ },
229
+ },
230
+ },
231
+ );
151
232
 
152
233
  if (!confirmLogout) {
153
234
  return;
@@ -157,10 +238,36 @@ export class SolidService extends Service {
157
238
  return;
158
239
  }
159
240
 
160
- this.setState({ previousSession: null });
161
- this.isLoggedIn() && (await this.authenticator.logout());
241
+ await UI.loading(translateWithDefault('solid.logoutOngoing', 'Logging out...'), async () => {
242
+ this.setState({
243
+ dismissed: false,
244
+ ignorePreviousSessionError: false,
245
+ loginStartupError: null,
246
+ previousSession: null,
247
+ ...DEFAULT_STATE,
248
+ });
249
+
250
+ this.isLoggedIn() && (await this.authenticator.logout());
162
251
 
163
- await Events.emit('logout');
252
+ if (options.wipeLocalData ?? true) {
253
+ await Storage.purge();
254
+ }
255
+
256
+ await Events.emit('auth:logout');
257
+ await Events.emit('auth:after-logout');
258
+ });
259
+ }
260
+
261
+ public async whenLoggedIn<T>(callback: (session: AuthSession) => T): Promise<T> {
262
+ if (this.isLoggedIn()) {
263
+ return callback(this.session);
264
+ }
265
+
266
+ return new Promise((resolve) => {
267
+ const onLogin = (session: AuthSession) => resolve(callback(session));
268
+
269
+ Events.once('auth:login', onLogin);
270
+ });
164
271
  }
165
272
 
166
273
  public dismiss(): void {
@@ -168,40 +275,48 @@ export class SolidService extends Service {
168
275
  }
169
276
 
170
277
  public async findOrCreatePrivateTypeIndex(): Promise<SolidTypeIndex> {
171
- if (!this.isLoggedIn()) {
172
- throw new Error('Can\'t find or create a type index because the user is not logged in');
278
+ return (await this.findPrivateTypeIndex()) ?? (await this.createPrivateTypeIndex());
279
+ }
280
+
281
+ public async findPrivateTypeIndex(options: { fresh?: boolean } = {}): Promise<SolidTypeIndex | null> {
282
+ const { webId, privateTypeIndexUrl } = this.requireUser();
283
+ const engine = this.requireAuthenticator().engine;
284
+
285
+ if (!privateTypeIndexUrl) {
286
+ return null;
173
287
  }
174
288
 
175
- const webId = this.user.webId;
289
+ if (options.fresh) {
290
+ resetAsyncMemo(`${webId}-privateTypeIndex`);
291
+ }
176
292
 
177
- return (this.privateTypeIndexes[webId] =
178
- this.privateTypeIndexes[webId] ??
179
- PromisedValue.from(
180
- (async () => (await this.findPrivateTypeIndex()) ?? (await this.createPrivateTypeIndex()))(),
181
- ));
293
+ return asyncMemo(`${webId}-privateTypeIndex`, () =>
294
+ SolidTypeIndex.withEngine(engine).find(privateTypeIndexUrl));
182
295
  }
183
296
 
184
297
  public async findOrCreatePublicTypeIndex(): Promise<SolidTypeIndex> {
185
- if (!this.isLoggedIn()) {
186
- throw new Error('Can\'t find or create a type index because the user is not logged in');
298
+ return (await this.findPublicTypeIndex()) ?? (await this.createPublicTypeIndex());
299
+ }
300
+
301
+ public async findPublicTypeIndex(options: { fresh?: boolean } = {}): Promise<SolidTypeIndex | null> {
302
+ const { webId, publicTypeIndexUrl } = this.requireUser();
303
+ const engine = this.requireAuthenticator().engine;
304
+
305
+ if (!publicTypeIndexUrl) {
306
+ return null;
187
307
  }
188
308
 
189
- const webId = this.user.webId;
309
+ if (options.fresh) {
310
+ resetAsyncMemo(`${webId}-publicTypeIndex`);
311
+ }
190
312
 
191
- return (this.publicTypeIndexes[webId] =
192
- this.publicTypeIndexes[webId] ??
193
- PromisedValue.from(
194
- (async () => (await this.findPublicTypeIndex()) ?? (await this.createPublicTypeIndex()))(),
195
- ));
313
+ return asyncMemo(`${webId}-publicTypeIndex`, () => SolidTypeIndex.withEngine(engine).find(publicTypeIndexUrl));
196
314
  }
197
315
 
198
316
  public async createPrivateContainer(options: {
199
317
  url: string;
200
- name: string;
201
- register?: {
202
- typeIndex: SolidTypeIndex;
203
- modelClass: SolidModelConstructor;
204
- };
318
+ name?: string;
319
+ registerFor?: SolidModelConstructor;
205
320
  reuseExisting?: boolean;
206
321
  }): Promise<SolidContainer> {
207
322
  const engine = this.requireAuthenticator().engine;
@@ -213,18 +328,21 @@ export class SolidService extends Service {
213
328
  throw new ContainerAlreadyInUse(existingContainer);
214
329
  }
215
330
 
216
- const container = existingContainer ?? new SolidContainer({ url: options.url, name: options.url });
331
+ const container = existingContainer ?? new SolidContainer({ url: options.url, name: options.name });
217
332
 
218
333
  await container.save();
219
334
 
220
- options.register && (await container.register(options.register.typeIndex.url, options.register.modelClass));
335
+ if (options.registerFor) {
336
+ const typeIndex = await this.findOrCreatePrivateTypeIndex();
337
+
338
+ await container.register(typeIndex, options.registerFor);
339
+ }
221
340
 
222
341
  return container;
223
342
  });
224
343
  }
225
344
 
226
- protected async boot(): Promise<void> {
227
- await super.boot();
345
+ protected override async boot(): Promise<void> {
228
346
  await Errors.booted;
229
347
 
230
348
  if (hasLocationQueryParameter('authenticator')) {
@@ -245,47 +363,99 @@ export class SolidService extends Service {
245
363
  await this.startupReconnect();
246
364
  }
247
365
 
248
- protected async findPrivateTypeIndex(): Promise<SolidTypeIndex | null> {
366
+ protected async createPrivateTypeIndex(): Promise<SolidTypeIndex> {
249
367
  const user = this.requireUser();
250
368
  const authenticator = this.requireAuthenticator();
369
+ const typeIndex = await SolidTypeIndex.withEngine(authenticator.engine).createPrivate(user);
370
+
371
+ return tap(typeIndex, () => {
372
+ user.privateTypeIndexUrl = typeIndex.url;
251
373
 
252
- return SolidTypeIndex.withEngine(authenticator.engine, () => SolidTypeIndex.find(user.privateTypeIndexUrl));
374
+ setAsyncMemo(`${user.webId}-privateTypeIndex`, typeIndex);
375
+
376
+ this.rememberProfile(user);
377
+ });
253
378
  }
254
379
 
255
- protected async createPrivateTypeIndex(): Promise<SolidTypeIndex> {
380
+ public async createPublicTypeIndex(): Promise<SolidTypeIndex> {
256
381
  const user = this.requireUser();
257
382
  const authenticator = this.requireAuthenticator();
258
- const typeIndex = await SolidTypeIndex.withEngine(authenticator.engine).createPrivate(user);
383
+ const typeIndex = await SolidTypeIndex.withEngine(authenticator.engine).createPublic(user);
259
384
 
260
385
  return tap(typeIndex, () => {
261
386
  user.privateTypeIndexUrl = typeIndex.url;
262
387
 
388
+ setAsyncMemo(`${user.webId}-publicTypeIndex`, typeIndex);
389
+
263
390
  this.rememberProfile(user);
264
391
  });
265
392
  }
266
393
 
267
- protected async findPublicTypeIndex(): Promise<SolidTypeIndex | null> {
268
- if (!this.user?.publicTypeIndexUrl) {
269
- return null;
270
- }
394
+ protected async attemptLogin(loginUrl: string, options: LoginOptions): Promise<boolean> {
395
+ const authenticatorName = options.authenticator ?? this.preferredAuthenticator ?? 'default';
396
+ const handleLoginError =
397
+ options.onError ??
398
+ ((error: ErrorSource) =>
399
+ App.isMounted() ? Errors.report(error) : this.setState({ loginStartupError: error }));
400
+ const staleTimeout = setTimeout(() => (this.loginStale = true), 10000);
401
+ const initialState = {
402
+ dismissed: this.dismissed,
403
+ ignorePreviousSessionError: this.ignorePreviousSessionError,
404
+ previousSession: this.previousSession,
405
+ };
271
406
 
272
- return SolidTypeIndex.find(this.user.publicTypeIndexUrl);
273
- }
407
+ this.loginOngoing = true;
274
408
 
275
- public async createPublicTypeIndex(): Promise<SolidTypeIndex> {
276
- if (!this.isLoggedIn()) {
277
- throw new Error('Can\'t create a type index because the user is not logged in');
278
- }
409
+ try {
410
+ const profile = await this.getUserProfile(loginUrl, { markStale: true });
411
+ const oidcIssuerUrl = profile?.oidcIssuerUrl ?? urlRoot(profile?.webId ?? loginUrl);
412
+ const authenticator = await this.bootAuthenticator(authenticatorName);
413
+ const { domain: loginDomain } = requireUrlParse(loginUrl);
414
+
415
+ this.setState({
416
+ dismissed: false,
417
+ ignorePreviousSessionError: true,
418
+ previousSession: objectWithoutEmpty({
419
+ profile,
420
+ loginUrl,
421
+ authenticator: authenticatorName,
422
+ error: translateWithDefault(
423
+ 'solid.stuckConnecting',
424
+ 'We didn\'t hear back from the identity provider at `{domain}`, maybe try reconnecting?',
425
+ { domain: loginDomain },
426
+ ),
427
+ }),
428
+ });
279
429
 
280
- const user = this.user;
281
- const typeIndex = await SolidTypeIndex.withEngine(this.authenticator.engine, () =>
282
- SolidTypeIndex.createPublic(user));
430
+ await Events.emit('auth:before-login');
283
431
 
284
- return tap(typeIndex, () => {
285
- user.publicTypeIndexUrl = typeIndex.url;
432
+ // This should redirect away from the app, so in most cases
433
+ // the rest of the code won't be reached.
434
+ await authenticator.login(oidcIssuerUrl, profile);
286
435
 
287
- this.rememberProfile(user);
288
- });
436
+ return true;
437
+ } catch (error) {
438
+ this.setState(initialState);
439
+
440
+ if (error instanceof AuthenticationCancelledError) {
441
+ return false;
442
+ }
443
+
444
+ if (options.fallbackUrl) {
445
+ return after().then(() => this.login(options.fallbackUrl ?? '', objectWithout(options, 'fallbackUrl')));
446
+ }
447
+
448
+ handleLoginError(error);
449
+
450
+ return false;
451
+ } finally {
452
+ clearTimeout(staleTimeout);
453
+
454
+ this.setState({
455
+ loginOngoing: false,
456
+ loginStale: false,
457
+ });
458
+ }
289
459
  }
290
460
 
291
461
  private async restorePreviousSession(): Promise<void> {
@@ -301,7 +471,7 @@ export class SolidService extends Service {
301
471
  return;
302
472
  }
303
473
 
304
- await this.reconnect();
474
+ await this.reconnect({ force: this.forcingReconnect() });
305
475
  }
306
476
 
307
477
  private reconnectOnStartup(): boolean {
@@ -312,6 +482,10 @@ export class SolidService extends Service {
312
482
  return this.autoReconnect;
313
483
  }
314
484
 
485
+ private forcingReconnect(): boolean {
486
+ return hasLocationQueryParameter('autoReconnect') && parseBoolean(getLocationQueryParameter('autoReconnect'));
487
+ }
488
+
315
489
  private async bootAuthenticator(name: AuthenticatorName): Promise<Authenticator> {
316
490
  const authenticator = getAuthenticator(name);
317
491
 
@@ -322,27 +496,28 @@ export class SolidService extends Service {
322
496
  previousSession: {
323
497
  authenticator: authenticator.name,
324
498
  loginUrl: session.loginUrl,
325
- avatarUrl: session.user.avatarUrl,
499
+ profile: session.user,
326
500
  error: null,
327
501
  },
328
502
  });
329
503
 
330
- if (
331
- session.user.cloaked ||
332
- !session.user.writableProfileUrl ||
333
- this.staleProfiles.includes(session.user.webId)
334
- ) {
504
+ if (this.staleProfiles.includes(session.user.webId)) {
335
505
  await this.refreshUserProfile();
336
506
  }
337
507
 
508
+ App.plugin('@aerogel/local-first') || setEngine(session.authenticator.engine);
509
+
338
510
  SolidACLAuthorization.setEngine(session.authenticator.engine);
511
+ SolidTypeIndex.setEngine(session.authenticator.engine);
512
+ SolidDocument.setEngine(session.authenticator.engine);
339
513
 
340
- await Events.emit('login', session);
514
+ await Events.emit('auth:login', session);
341
515
  },
342
516
  onSessionFailed: async (loginUrl, error) => {
343
517
  this.setState({
344
518
  session: null,
345
519
  previousSession: {
520
+ profile: this.previousSession?.profile,
346
521
  authenticator: authenticator.name,
347
522
  loginUrl,
348
523
  error,
@@ -350,7 +525,7 @@ export class SolidService extends Service {
350
525
  });
351
526
  },
352
527
  onSessionEnded: () => this.setState({ session: null }),
353
- onAuthenticatedFetchReady: (fetch) => Events.emit('authenticated-fetch-ready', fetch),
528
+ onAuthenticatedFetchReady: (fetch) => Events.emit('auth:fetch-ready', fetch),
354
529
  });
355
530
 
356
531
  await authenticator.boot();
@@ -370,12 +545,15 @@ export class SolidService extends Service {
370
545
 
371
546
  }
372
547
 
373
- export default facade(new SolidService());
548
+ export default facade(SolidService);
374
549
 
375
550
  declare module '@aerogel/core' {
376
551
  export interface EventsPayload {
377
- 'authenticated-fetch-ready': Fetch;
378
- login: AuthSession;
379
- logout: void;
552
+ 'auth:fetch-ready': Fetch;
553
+ 'auth:before-login': void;
554
+ 'auth:login': AuthSession;
555
+ 'auth:logout': void;
556
+ 'auth:after-logout': void;
557
+ 'solid:user-profile-loaded': [SolidUserProfile, SolidStore];
380
558
  }
381
559
  }
@@ -0,0 +1,11 @@
1
+ import { FakeLocalStorage } from '@noeldemartin/testing';
2
+ import { vi } from 'vitest';
3
+
4
+ vi.mock('@aerogel/core', async () => {
5
+ const original = (await vi.importActual('@aerogel/core')) as object;
6
+
7
+ FakeLocalStorage.reset();
8
+ FakeLocalStorage.patchGlobal();
9
+
10
+ return original;
11
+ });
@@ -0,0 +1 @@
1
+ /// <reference types="@aerogel/plugin-local-first" />
@@ -0,0 +1,21 @@
1
+ declare module 'solid-auth-client' {
2
+ export type Session = {
3
+ webId: string;
4
+ };
5
+
6
+ export class SolidAuthClient {
7
+
8
+ public fetch(...args: any[]): Promise<Response>;
9
+
10
+ public login(loginUrl: string): Promise<Session | void>;
11
+
12
+ public currentSession(): Promise<Session | void>;
13
+
14
+ public logout(): Promise<void>;
15
+
16
+ }
17
+
18
+ const auth: SolidAuthClient;
19
+
20
+ export default auth;
21
+ }