@aerogel/plugin-solid 0.0.1-next.33c058471a47b828f174aca4d2b5aeadfeb2c627

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.
@@ -0,0 +1,123 @@
1
+ import { PromisedValue, arr, fail } from '@noeldemartin/utils';
2
+ import { SolidEngine } from 'soukai-solid';
3
+ import type { Closure, FluentArray, GetClosureArgs } from '@noeldemartin/utils';
4
+ import type { Engine } from 'soukai';
5
+ import type { Fetch } from 'soukai-solid';
6
+ import type { SolidUserProfile } from '@noeldemartin/solid-utils';
7
+ import type { ErrorSource } from '@aerogel/core';
8
+
9
+ import type { AuthenticatorName } from '@/auth';
10
+
11
+ type ListenerEvent = keyof AuthenticatorListener;
12
+ type ListenerEventPayload<
13
+ Event extends ListenerEvent,
14
+ Callback = AuthenticatorListener[Event]
15
+ > = Callback extends Closure ? GetClosureArgs<Callback> : never;
16
+
17
+ export interface AuthSession {
18
+ user: SolidUserProfile;
19
+ loginUrl: string;
20
+ authenticator: Authenticator;
21
+ }
22
+
23
+ export interface AuthenticatorListener {
24
+ onSessionStarted?: (session: AuthSession) => Promise<void> | void;
25
+ onSessionFailed?: (loginUrl: string, error: ErrorSource) => Promise<void> | void;
26
+ onSessionEnded?: () => Promise<void> | void;
27
+ onAuthenticatedFetchReady?: (fetch: Fetch) => Promise<void> | void;
28
+ }
29
+
30
+ export default abstract class Authenticator {
31
+
32
+ public name!: AuthenticatorName;
33
+
34
+ private authenticatedFetch?: Fetch;
35
+ private booted?: PromisedValue<void>;
36
+ private listeners: FluentArray<AuthenticatorListener> = arr<AuthenticatorListener>([]);
37
+ private _engine?: SolidEngine;
38
+
39
+ public abstract login(loginUrl: string, user?: SolidUserProfile): Promise<AuthSession>;
40
+
41
+ public abstract logout(): Promise<void>;
42
+
43
+ public get engine(): Engine {
44
+ this._engine = this._engine ?? new SolidEngine(this.requireAuthenticatedFetch());
45
+
46
+ return this._engine;
47
+ }
48
+
49
+ public getAuthenticatedFetch(): Fetch | null {
50
+ return this.authenticatedFetch ?? null;
51
+ }
52
+
53
+ public requireAuthenticatedFetch(): Fetch {
54
+ return this.authenticatedFetch ?? fail('Authenticated fetch is not ready');
55
+ }
56
+
57
+ public addListener(listener: AuthenticatorListener): () => void {
58
+ if (!this.listeners.includes(listener)) this.listeners.push(listener);
59
+
60
+ return () => {
61
+ this.removeListener(listener);
62
+ };
63
+ }
64
+
65
+ public removeListener(listener: AuthenticatorListener): void {
66
+ if (!this.listeners.includes(listener)) return;
67
+
68
+ this.listeners.remove(listener);
69
+ }
70
+
71
+ public async boot(): Promise<void> {
72
+ if (this.booted) {
73
+ return this.booted;
74
+ }
75
+
76
+ this.booted = new PromisedValue();
77
+
78
+ await this.restoreSession();
79
+
80
+ this.booted.resolve();
81
+ }
82
+
83
+ protected abstract restoreSession(): Promise<void>;
84
+
85
+ protected async initAuthenticatedFetch(fetch: Fetch): Promise<void> {
86
+ this.authenticatedFetch = fetch;
87
+
88
+ await this.notifyListeners('onAuthenticatedFetchReady', fetch);
89
+ }
90
+
91
+ protected async startSession(sessionData: Omit<AuthSession, 'authenticator'>): Promise<AuthSession> {
92
+ const session: AuthSession = {
93
+ authenticator: this,
94
+ ...sessionData,
95
+ };
96
+
97
+ await this.notifyListeners('onSessionStarted', session);
98
+
99
+ return session;
100
+ }
101
+
102
+ protected async failSession(loginUrl: string, error: ErrorSource): Promise<void> {
103
+ await this.notifyListeners('onSessionFailed', loginUrl, error);
104
+ }
105
+
106
+ protected async endSession(): Promise<void> {
107
+ await this.notifyListeners('onSessionEnded');
108
+ }
109
+
110
+ protected async notifyListeners<Event extends ListenerEvent>(
111
+ event: Event,
112
+ ...payload: ListenerEventPayload<Event>
113
+ ): Promise<void> {
114
+ await Promise.all(
115
+ this.listeners.toArray().map(async (listener) => {
116
+ const callback = listener[event] as Closure | undefined;
117
+
118
+ await callback?.(...payload);
119
+ }),
120
+ );
121
+ }
122
+
123
+ }
@@ -0,0 +1 @@
1
+ export { fetch, handleIncomingRedirect, login, logout } from '@inrupt/solid-client-authn-browser';
@@ -0,0 +1,74 @@
1
+ import { Storage, after, fail } from '@noeldemartin/utils';
2
+ import type { Fetch } from 'soukai-solid';
3
+ import type { handleIncomingRedirect, login, logout } from '@inrupt/solid-client-authn-browser';
4
+
5
+ import Authenticator from '@/auth/Authenticator';
6
+ import type { AuthSession } from '@/auth/Authenticator';
7
+
8
+ const STORAGE_KEY = 'inrupt-authenticator';
9
+
10
+ export default class InruptAuthenticator extends Authenticator {
11
+
12
+ private _fetch!: Fetch;
13
+ private _login!: typeof login;
14
+ private _logout!: typeof logout;
15
+ private _handleIncomingRedirect!: typeof handleIncomingRedirect;
16
+
17
+ public async login(loginUrl: string): Promise<AuthSession> {
18
+ Storage.set<boolean>(STORAGE_KEY, true);
19
+
20
+ await this._login({
21
+ oidcIssuer: loginUrl,
22
+ // TODO clientId, clientName, and redirectUrl
23
+ });
24
+
25
+ // Browser should redirect, so just make it wait for a while.
26
+ await after({ seconds: 60 });
27
+
28
+ return fail('Browser should have redirected, but it didn\'t');
29
+ }
30
+
31
+ public async logout(): Promise<void> {
32
+ await this._logout();
33
+ await this.endSession();
34
+ }
35
+
36
+ protected async restoreSession(): Promise<void> {
37
+ const { fetch, handleIncomingRedirect, login, logout } = await import('./InruptAuthenticator.lazy');
38
+
39
+ this._fetch = fetch;
40
+ this._login = login;
41
+ this._logout = logout;
42
+ this._handleIncomingRedirect = handleIncomingRedirect;
43
+
44
+ await this.loginFromRedirect();
45
+ }
46
+
47
+ protected async loginFromRedirect(): Promise<void> {
48
+ if (!Storage.has(STORAGE_KEY)) {
49
+ return;
50
+ }
51
+
52
+ const session = await this._handleIncomingRedirect(window.location.href);
53
+
54
+ Storage.remove(STORAGE_KEY);
55
+
56
+ if (session?.isLoggedIn && session.webId) {
57
+ await this.initSession(session.webId);
58
+ }
59
+ }
60
+
61
+ protected async initSession(webId: string): Promise<void> {
62
+ await this.initAuthenticatedFetch(this._fetch);
63
+
64
+ try {
65
+ const { default: Solid } = await import('@/services/Solid');
66
+ const user = await Solid.requireUserProfile(webId);
67
+
68
+ await this.startSession({ user, loginUrl: webId });
69
+ } catch (error) {
70
+ await this.failSession(webId, error);
71
+ }
72
+ }
73
+
74
+ }
@@ -0,0 +1,38 @@
1
+ import InruptAuthenticator from './authenticators/InruptAuthenticator';
2
+ import type Authenticator from './Authenticator';
3
+
4
+ const _authenticators = {} as Authenticators;
5
+
6
+ type BaseAuthenticators = typeof authenticators;
7
+
8
+ export * from './Authenticator';
9
+ export { Authenticator };
10
+
11
+ export const authenticators = {
12
+ inrupt: new InruptAuthenticator(),
13
+ };
14
+
15
+ export function setDefaultAuthenticator(authenticator: Authenticator): void {
16
+ _authenticators.default = authenticator;
17
+ }
18
+
19
+ export function getAuthenticator<T extends AuthenticatorName>(name: T): Authenticators[T] {
20
+ return _authenticators[name];
21
+ }
22
+
23
+ export function registerAuthenticators(customAuthenticators: Record<string, Authenticator>): void {
24
+ Object.entries(customAuthenticators).forEach(([name, authenticator]) =>
25
+ registerAuthenticator(name as AuthenticatorName, authenticator));
26
+ }
27
+
28
+ export function registerAuthenticator<T extends AuthenticatorName>(name: T, authenticator: Authenticators[T]): void {
29
+ _authenticators[name] = authenticator;
30
+
31
+ authenticator.name = authenticator.name ?? name;
32
+ }
33
+
34
+ export type AuthenticatorName = keyof Authenticators;
35
+
36
+ export interface Authenticators extends BaseAuthenticators {
37
+ default: Authenticator;
38
+ }
@@ -0,0 +1,5 @@
1
+ import type { AuthSession } from '@/auth/Authenticator';
2
+
3
+ export interface IAGSolidStatusLoggedInSlotProps {
4
+ session: AuthSession;
5
+ }
@@ -0,0 +1,60 @@
1
+ <template>
2
+ <div>
3
+ <slot v-if="$solid.loginStale" name="login-stale">
4
+ <AGMarkdown :text="$td('solid.loginStale', 'This is taking too long...')" />
5
+ </slot>
6
+
7
+ <slot v-if="$solid.loginOngoing" name="login-ongoing">
8
+ <AGMarkdown :text="$td('ui.loading', 'Loading...')" />
9
+ </slot>
10
+
11
+ <slot v-else-if="$solid.isLoggedIn()" name="logged-in" :session="$solid.session">
12
+ <div class="flex flex-col gap-3">
13
+ <AGMarkdown
14
+ :text="
15
+ $td(
16
+ 'solid.loggedIn',
17
+ {
18
+ userName: $solid.user.name ?? $solid.user.webId,
19
+ userWebId: $solid.user.webId,
20
+ },
21
+ 'You are logged in as [{userName}]({userWebId})'
22
+ )
23
+ "
24
+ />
25
+ <AGButton @click="$solid.logout()">
26
+ {{ $td('solid.logout', 'Logout') }}
27
+ </AGButton>
28
+ </div>
29
+ </slot>
30
+
31
+ <slot v-else name="logged-out">
32
+ <div class="flex flex-col">
33
+ <AGForm :form="form" class="flex" @submit="$solid.login(form.url)">
34
+ <AGInput
35
+ name="url"
36
+ :aria-label="$td('solid.url', 'Login url')"
37
+ :placeholder="$td('solid.placeholder', 'https://...')"
38
+ />
39
+ <AGButton submit>
40
+ {{ $td('solid.login', 'Login') }}
41
+ </AGButton>
42
+ </AGForm>
43
+ <AGButton v-if="$solid.wasLoggedIn" class="mt-3" @click="$solid.reconnect({ force: true })">
44
+ {{ $td('solid.reconnect', 'Reconnect') }}
45
+ </AGButton>
46
+ <AGMarkdown
47
+ v-if="$solid.previousSession?.error"
48
+ class="mt-1 self-center text-sm text-red-800"
49
+ :text="$td('solid.previousLoginError', 'Previous login attempt failed')"
50
+ />
51
+ </div>
52
+ </slot>
53
+ </div>
54
+ </template>
55
+
56
+ <script setup lang="ts">
57
+ import { AGButton, AGForm, AGInput, AGMarkdown, requiredStringInput, useForm } from '@aerogel/core';
58
+
59
+ const form = useForm({ url: requiredStringInput() });
60
+ </script>
@@ -0,0 +1,4 @@
1
+ import AGSolidStatus from './AGSolidStatus.vue';
2
+ import { IAGSolidStatusLoggedInSlotProps } from './AGSolidStatus';
3
+
4
+ export { AGSolidStatus, IAGSolidStatusLoggedInSlotProps };
@@ -0,0 +1,3 @@
1
+ import { JSError } from '@noeldemartin/utils';
2
+
3
+ export default class AuthenticationCancelledError extends JSError {}
@@ -0,0 +1,11 @@
1
+ import { JSError } from '@noeldemartin/utils';
2
+ import type { JSErrorOptions } from '@noeldemartin/utils';
3
+ import type { SolidContainer } from 'soukai-solid';
4
+
5
+ export default class ContainerAlreadyInUse extends JSError {
6
+
7
+ constructor(container: SolidContainer, options?: JSErrorOptions) {
8
+ super(`Container at ${container.url} is already in use`, options);
9
+ }
10
+
11
+ }
@@ -0,0 +1,4 @@
1
+ import AuthenticationCancelledError from '@/errors/AuthenticationCancelledError';
2
+ import ContainerAlreadyInUse from '@/errors/ContainerAlreadyInUse';
3
+
4
+ export { AuthenticationCancelledError, ContainerAlreadyInUse };
package/src/main.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { bootServices } from '@aerogel/core';
2
+ import { bootSolidModels } from 'soukai-solid';
3
+ import type { Plugin } from '@aerogel/core';
4
+
5
+ import Solid from '@/services/Solid';
6
+ import {
7
+ authenticators as baseAuthenticators,
8
+ getAuthenticator,
9
+ registerAuthenticators,
10
+ setDefaultAuthenticator,
11
+ } from '@/auth';
12
+ import type Authenticator from '@/auth/Authenticator';
13
+ import type { AuthenticatorName } from '@/auth';
14
+
15
+ const services = { $solid: Solid };
16
+
17
+ export { Solid };
18
+ export * from './auth';
19
+ export * from './components';
20
+ export * from './errors';
21
+
22
+ export interface Options {
23
+ authenticators?: Record<string, Authenticator>;
24
+ defaultAuthenticator?: AuthenticatorName;
25
+ }
26
+
27
+ export type SolidServices = typeof services;
28
+
29
+ export default function solid(options: Options = {}): Plugin {
30
+ return {
31
+ async install(app) {
32
+ bootSolidModels();
33
+ registerAuthenticators({ ...baseAuthenticators, ...(options.authenticators ?? {}) });
34
+ setDefaultAuthenticator(getAuthenticator(options.defaultAuthenticator ?? 'inrupt'));
35
+
36
+ await bootServices(app, services);
37
+ },
38
+ };
39
+ }
40
+
41
+ declare module '@vue/runtime-core' {
42
+ interface ComponentCustomProperties extends SolidServices {}
43
+ }
@@ -0,0 +1,60 @@
1
+ import { defineServiceState } from '@aerogel/core';
2
+ import type { ErrorSource } from '@aerogel/core';
3
+ import type { Fetch, SolidUserProfile } from '@noeldemartin/solid-utils';
4
+
5
+ import type { AuthSession } from '@/auth/Authenticator';
6
+ import type { AuthenticatorName } from '@/auth';
7
+
8
+ export default defineServiceState({
9
+ name: 'solid',
10
+ persist: ['autoReconnect', 'dismissed', 'previousSession', 'profiles', 'staleProfiles'],
11
+ initialState: {
12
+ autoReconnect: false,
13
+ dismissed: false,
14
+ ignorePreviousSessionError: false,
15
+ loginStartupError: null as ErrorSource | null,
16
+ loginOngoing: false,
17
+ preferredAuthenticator: null as AuthenticatorName | null,
18
+ previousSession: null as {
19
+ authenticator: AuthenticatorName;
20
+ loginUrl: string;
21
+ avatarUrl?: string;
22
+ error: ErrorSource | null;
23
+ } | null,
24
+ profiles: {} as Record<string, SolidUserProfile>,
25
+ session: null as AuthSession | null,
26
+ loginStale: false,
27
+ staleProfiles: [] as string[],
28
+ },
29
+ serialize(state) {
30
+ if (state.previousSession?.error instanceof Error) {
31
+ state.previousSession.error = state.previousSession.error.message;
32
+ }
33
+
34
+ return state;
35
+ },
36
+ computed: {
37
+ loggedIn: (state) => !!state.session,
38
+ user: (state) => state.session?.user ?? null,
39
+ userAvatarUrl: (state) => state.session?.user?.avatarUrl ?? state.previousSession?.avatarUrl ?? null,
40
+ wasLoggedIn: (state) => !!state.previousSession?.loginUrl,
41
+ authenticator: (state) => state.session?.authenticator ?? null,
42
+ hasLoggedIn(): boolean {
43
+ return this.loggedIn || this.wasLoggedIn;
44
+ },
45
+ fetch(): Fetch {
46
+ return this.authenticator?.getAuthenticatedFetch() ?? window.fetch.bind(window);
47
+ },
48
+ error(state): ErrorSource | null {
49
+ if (state.loginStartupError) {
50
+ return state.loginStartupError ?? null;
51
+ }
52
+
53
+ if (state.ignorePreviousSessionError) {
54
+ return null;
55
+ }
56
+
57
+ return state.previousSession?.error ?? null;
58
+ },
59
+ },
60
+ });