sol-components 2.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.
- package/README.md +7 -0
- package/core/activate.js +27 -0
- package/core/adopt.js +71 -0
- package/core/auth-core.js +73 -0
- package/core/auth-fetch.js +154 -0
- package/core/component-mount.js +110 -0
- package/core/defaults.js +48 -0
- package/core/define.js +15 -0
- package/core/display-target.js +166 -0
- package/core/edit-placements.js +28 -0
- package/core/editor-self.js +127 -0
- package/core/editor.js +162 -0
- package/core/events.js +27 -0
- package/core/extension-points.js +189 -0
- package/core/form-utils.js +210 -0
- package/core/from-query.js +138 -0
- package/core/from-rdf.js +52 -0
- package/core/here.js +33 -0
- package/core/include-core.js +73 -0
- package/core/inrupt-global.js +18 -0
- package/core/menu-consumer.js +41 -0
- package/core/menu-rdf.js +154 -0
- package/core/pod-ops.js +392 -0
- package/core/pod-registry.js +82 -0
- package/core/popup-proxy.js +255 -0
- package/core/rdf-core.js +280 -0
- package/core/rdf-render.js +136 -0
- package/core/rdf-utils.js +411 -0
- package/core/rdf.js +154 -0
- package/core/services.js +106 -0
- package/core/shape-to-form.js +741 -0
- package/core/sparql-safety.js +20 -0
- package/core/utils.js +196 -0
- package/dist/importmap-cdn.json +49 -0
- package/dist/importmap-local.json +49 -0
- package/dist/sol-loader.manifest.json +140 -0
- package/dist/vendor/@comunica-query-sparql.js +137851 -0
- package/dist/vendor/@inrupt-solid-client-authn-browser.js +7503 -0
- package/dist/vendor/dompurify.js +1476 -0
- package/dist/vendor/ical.js.js +9739 -0
- package/dist/vendor/marked.js +85 -0
- package/dist/vendor/n3.js +14670 -0
- package/dist/vendor/rdf-validate-shacl.js +6970 -0
- package/dist/vendor/rdflib.js +35172 -0
- package/dist/vendor/solid-logic.js +6819 -0
- package/dist/vendor/solid-ui.js +21945 -0
- package/node/sol-form.js +133 -0
- package/node/sol-include.js +55 -0
- package/node/sol-login.js +632 -0
- package/node/sol-menu.js +639 -0
- package/node/sol-query.js +116 -0
- package/package.json +133 -0
- package/web/menu-from-rdf.js +23 -0
- package/web/scripts/prefs.js +25 -0
- package/web/sol-accordion.js +114 -0
- package/web/sol-basic.js +50 -0
- package/web/sol-breadcrumb.js +131 -0
- package/web/sol-button.js +244 -0
- package/web/sol-calendar.js +465 -0
- package/web/sol-default.js +118 -0
- package/web/sol-dropdown-button.js +222 -0
- package/web/sol-feed.js +1336 -0
- package/web/sol-form.js +949 -0
- package/web/sol-full.js +43 -0
- package/web/sol-gallery.js +303 -0
- package/web/sol-include.js +246 -0
- package/web/sol-live-edit.js +415 -0
- package/web/sol-login.js +856 -0
- package/web/sol-menu.js +593 -0
- package/web/sol-modal.js +377 -0
- package/web/sol-pod-extras.js +17 -0
- package/web/sol-pod-ops.js +680 -0
- package/web/sol-pod.js +1039 -0
- package/web/sol-query.js +546 -0
- package/web/sol-rolodex.js +95 -0
- package/web/sol-search.js +402 -0
- package/web/sol-settings.js +199 -0
- package/web/sol-solidos.js +93 -0
- package/web/sol-tabs.js +445 -0
- package/web/sol-time.js +194 -0
- package/web/sol-tree-edit.js +492 -0
- package/web/sol-wac.js +456 -0
- package/web/sol-weather.js +337 -0
- package/web/sol-window.js +142 -0
- package/web/styles/buttons-css.js +108 -0
- package/web/styles/help.css +242 -0
- package/web/styles/root.css +112 -0
- package/web/styles/sol-accordion-css.js +97 -0
- package/web/styles/sol-calendar-css.js +154 -0
- package/web/styles/sol-feed-css.js +475 -0
- package/web/styles/sol-form-css.js +471 -0
- package/web/styles/sol-gallery-css.js +181 -0
- package/web/styles/sol-include-css.js +95 -0
- package/web/styles/sol-live-edit-css.js +84 -0
- package/web/styles/sol-live-edit.css +101 -0
- package/web/styles/sol-login-css.js +116 -0
- package/web/styles/sol-menu-css.js +145 -0
- package/web/styles/sol-modal-css.js +134 -0
- package/web/styles/sol-pod-css.js +187 -0
- package/web/styles/sol-pod-modal-css.js +203 -0
- package/web/styles/sol-query-css.js +140 -0
- package/web/styles/sol-query-help.css +267 -0
- package/web/styles/sol-query-one-pager.css +67 -0
- package/web/styles/sol-search-css.js +157 -0
- package/web/styles/sol-solidos-css.js +7 -0
- package/web/styles/sol-tabs-css.js +114 -0
- package/web/styles/sol-time-css.js +30 -0
- package/web/styles/sol-wac-css.js +73 -0
- package/web/styles/sol-weather-css.js +59 -0
- package/web/styles/solid-logo.svg +9 -0
- package/web/styles/view-accordion-css.js +66 -0
- package/web/styles/view-anchorlist-css.js +22 -0
- package/web/styles/view-autocomplete-css.js +59 -0
- package/web/styles/view-rolodex-css.js +102 -0
- package/web/styles/view-select-css.js +21 -0
- package/web/utils/calendar-fetch.js +388 -0
- package/web/utils/code-mirror-editor.js +82 -0
- package/web/utils/commons-fetch.js +108 -0
- package/web/utils/feed-edit.js +159 -0
- package/web/utils/feed-edit.smoke.mjs +74 -0
- package/web/utils/feed-fetch.js +573 -0
- package/web/utils/live-edit-help/csv.js +64 -0
- package/web/utils/live-edit-help/graphviz.js +41 -0
- package/web/utils/live-edit-help/jsonld.js +55 -0
- package/web/utils/live-edit-help/markdown.js +52 -0
- package/web/utils/live-edit-help/mermaid.js +48 -0
- package/web/utils/live-edit-help/turtle.js +85 -0
- package/web/utils/rdf-config.js +125 -0
- package/web/utils/renderers/csv.js +124 -0
- package/web/utils/renderers/d3-force.js +82 -0
- package/web/utils/renderers/graphviz.js +13 -0
- package/web/utils/renderers/html.js +10 -0
- package/web/utils/renderers/jsonld.js +63 -0
- package/web/utils/renderers/markdown.js +19 -0
- package/web/utils/renderers/mermaid.js +54 -0
- package/web/utils/renderers/turtle.js +51 -0
- package/web/utils/sol-query-triple-patterns.js +151 -0
- package/web/utils/sol-query-ui.js +250 -0
- package/web/utils/sol-query-views.js +32 -0
- package/web/views/_helpers.js +34 -0
- package/web/views/accordion.js +133 -0
- package/web/views/anchorlist.js +59 -0
- package/web/views/auto-complete.js +183 -0
- package/web/views/dl.js +38 -0
- package/web/views/list.js +19 -0
- package/web/views/menu.js +56 -0
- package/web/views/rolodex.js +126 -0
- package/web/views/select.js +79 -0
- package/web/views/table.js +73 -0
- package/web/views/tabs.js +57 -0
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js authentication manager for Solid Pods.
|
|
3
|
+
*
|
|
4
|
+
* Provides the same session management as the `<sol-login>` web component
|
|
5
|
+
* but for scripts and servers — no DOM, localStorage, or window dependencies.
|
|
6
|
+
*
|
|
7
|
+
* @module sol-login-node
|
|
8
|
+
* @example
|
|
9
|
+
* import { SolidAuth } from 'sol-components/login-node';
|
|
10
|
+
* const auth = new SolidAuth();
|
|
11
|
+
*
|
|
12
|
+
* // Username/password (NSS & CSS — no browser needed):
|
|
13
|
+
* const session = await auth.login({
|
|
14
|
+
* oidcIssuer: 'https://solidcommunity.net',
|
|
15
|
+
* username: 'alice',
|
|
16
|
+
* password: 'secret',
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* // Interactive browser login (default when no credentials given):
|
|
20
|
+
* const session = await auth.login({ oidcIssuer: 'https://solidcommunity.net' });
|
|
21
|
+
*
|
|
22
|
+
* // Client credentials (scripts, bots, CI):
|
|
23
|
+
* const session = await auth.login({
|
|
24
|
+
* oidcIssuer: 'https://solidcommunity.net',
|
|
25
|
+
* clientId: 'my-app-id',
|
|
26
|
+
* clientSecret: 'my-secret',
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* // session.fetch handles local files and remote URLs:
|
|
30
|
+
* const resp = await session.fetch('https://alice.solidcommunity.net/pod/file.ttl');
|
|
31
|
+
* const local = await session.fetch('./data.ttl');
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
originOf,
|
|
36
|
+
sessionCoversOrigin,
|
|
37
|
+
isNoAuth as _isNoAuth,
|
|
38
|
+
getSessionFor as _getSessionFor,
|
|
39
|
+
makeFetchFor,
|
|
40
|
+
isLoggedInFor,
|
|
41
|
+
getWebId as _getWebId,
|
|
42
|
+
getFirstLoggedIn as _getFirstLoggedIn,
|
|
43
|
+
} from '../core/auth-core.js';
|
|
44
|
+
|
|
45
|
+
let _SessionClass = null;
|
|
46
|
+
|
|
47
|
+
async function getSessionClass() {
|
|
48
|
+
if (_SessionClass) return _SessionClass;
|
|
49
|
+
const mod = await import('@inrupt/solid-client-authn-node');
|
|
50
|
+
_SessionClass = mod.Session;
|
|
51
|
+
if (!_SessionClass) throw new Error('sol-login-node: @inrupt/solid-client-authn-node must be installed');
|
|
52
|
+
return _SessionClass;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function _defaultTokenPath() {
|
|
56
|
+
const home = process.env.HOME || process.env.USERPROFILE || '.';
|
|
57
|
+
return `${home}/.solid-auth-tokens.json`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function _readTokenStore(path) {
|
|
61
|
+
try {
|
|
62
|
+
const fs = await import('node:fs/promises');
|
|
63
|
+
const data = await fs.readFile(path, 'utf8');
|
|
64
|
+
return JSON.parse(data);
|
|
65
|
+
} catch { return {}; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function _writeTokenStore(path, store) {
|
|
69
|
+
const fs = await import('node:fs/promises');
|
|
70
|
+
await fs.writeFile(path, JSON.stringify(store, null, 2), { mode: 0o600 });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function _openBrowser(url) {
|
|
74
|
+
const { platform } = await import('node:os');
|
|
75
|
+
const { exec } = await import('node:child_process');
|
|
76
|
+
const cmd = platform() === 'darwin' ? 'open'
|
|
77
|
+
: platform() === 'win32' ? 'start'
|
|
78
|
+
: 'xdg-open';
|
|
79
|
+
exec(`${cmd} ${JSON.stringify(url)}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function _startCallbackServer(port = 0) {
|
|
83
|
+
const http = await import('node:http');
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
const server = http.createServer();
|
|
86
|
+
server.listen(port, 'localhost', () => {
|
|
87
|
+
const addr = server.address();
|
|
88
|
+
resolve({ server, port: addr.port });
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Manages Solid authentication sessions for Node.js environments.
|
|
95
|
+
*
|
|
96
|
+
* Supports three login strategies: client credentials (for scripts/bots/CI),
|
|
97
|
+
* username/password (NSS & CSS servers), and interactive browser-based OIDC.
|
|
98
|
+
* Sessions are stored in a Map keyed by tag and can be looked up by URL origin.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* const auth = new SolidAuth({ noAuth: 'http://localhost:3000' });
|
|
102
|
+
* await auth.login({ oidcIssuer: 'https://solidcommunity.net', clientId: 'id', clientSecret: 'secret' });
|
|
103
|
+
* const resp = await auth.fetch('https://alice.solidcommunity.net/pod/file.ttl');
|
|
104
|
+
*/
|
|
105
|
+
export class SolidAuth {
|
|
106
|
+
/**
|
|
107
|
+
* @param {Object} [opts]
|
|
108
|
+
* @param {string|string[]} [opts.noAuth] - Origin(s) that never need authentication.
|
|
109
|
+
* @param {string|null} [opts.tokenStore] - Path to the JSON token file for session persistence.
|
|
110
|
+
* Defaults to `~/.solid-auth-tokens.json`. Pass `null` to disable persistence.
|
|
111
|
+
*/
|
|
112
|
+
constructor(opts = {}) {
|
|
113
|
+
/** @type {Map<string, Object>} */
|
|
114
|
+
this.sessions = new Map();
|
|
115
|
+
this._noAuth = opts.noAuth || null;
|
|
116
|
+
this._tokenStore = opts.tokenStore !== undefined ? opts.tokenStore : _defaultTokenPath();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** @param {string|string[]} v - Origin(s) that bypass authentication. */
|
|
120
|
+
set noAuth(v) { this._noAuth = v; }
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check whether a URL's origin is in the noAuth list.
|
|
124
|
+
* @param {string} url
|
|
125
|
+
* @returns {boolean}
|
|
126
|
+
*/
|
|
127
|
+
isNoAuth(url) { return _isNoAuth(url, this._noAuth); }
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Extract the origin (scheme + host + port) from a URL string.
|
|
131
|
+
* @param {string} url
|
|
132
|
+
* @returns {string} The origin, or empty string if not matched.
|
|
133
|
+
*/
|
|
134
|
+
originOf(url) { return originOf(url); }
|
|
135
|
+
|
|
136
|
+
_sessionCoversOrigin(session, origin) {
|
|
137
|
+
return sessionCoversOrigin(session, origin);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
_makeSessionResult(session) {
|
|
141
|
+
const self = this;
|
|
142
|
+
return {
|
|
143
|
+
webId: session.info.webId,
|
|
144
|
+
isLoggedIn: session.info.isLoggedIn,
|
|
145
|
+
fetch: (url, init) => self.fetch(url, init),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Fetch a resource, using an authenticated session when available.
|
|
151
|
+
* Local paths (starting with `/` or `./`) are handled via the filesystem.
|
|
152
|
+
* @param {string} url - Remote URL or local file path.
|
|
153
|
+
* @param {RequestInit} [init={}]
|
|
154
|
+
* @returns {Promise<Response>}
|
|
155
|
+
*/
|
|
156
|
+
async fetch(url, init = {}) {
|
|
157
|
+
if (typeof url === 'string' && (url.startsWith('/') || url.startsWith('./'))) {
|
|
158
|
+
return this._localFetch(url, init);
|
|
159
|
+
}
|
|
160
|
+
return this.fetchFor(url)(url, init);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async _localFetch(url, init = {}) {
|
|
164
|
+
const { resolve } = await import('node:path');
|
|
165
|
+
const { readFile, writeFile, unlink, mkdir, stat } = await import('node:fs/promises');
|
|
166
|
+
const filePath = resolve(url);
|
|
167
|
+
const method = (init.method || 'GET').toUpperCase();
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
if (method === 'GET' || method === 'HEAD') {
|
|
171
|
+
const content = await readFile(filePath, 'utf8');
|
|
172
|
+
return new Response(method === 'HEAD' ? null : content, {
|
|
173
|
+
status: 200,
|
|
174
|
+
headers: { 'content-type': 'text/plain' },
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (method === 'PUT' || method === 'POST') {
|
|
178
|
+
const { dirname } = await import('node:path');
|
|
179
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
180
|
+
await writeFile(filePath, init.body || '', 'utf8');
|
|
181
|
+
return new Response(null, { status: method === 'PUT' ? 201 : 200 });
|
|
182
|
+
}
|
|
183
|
+
if (method === 'DELETE') {
|
|
184
|
+
await unlink(filePath);
|
|
185
|
+
return new Response(null, { status: 204 });
|
|
186
|
+
}
|
|
187
|
+
return new Response(null, { status: 405, statusText: 'Method Not Allowed' });
|
|
188
|
+
} catch (e) {
|
|
189
|
+
if (e.code === 'ENOENT') return new Response(null, { status: 404, statusText: 'Not Found' });
|
|
190
|
+
return new Response(e.message, { status: 500 });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Authenticate with a Solid identity provider.
|
|
196
|
+
*
|
|
197
|
+
* Strategy selection:
|
|
198
|
+
* - If `clientId` + `clientSecret` are provided, uses client-credentials flow.
|
|
199
|
+
* - Otherwise tries to restore a saved session, then username/password, then interactive browser login.
|
|
200
|
+
*
|
|
201
|
+
* @param {Object} [opts]
|
|
202
|
+
* @param {string} opts.oidcIssuer - The Solid OIDC issuer URL.
|
|
203
|
+
* @param {string} [opts.tag='default'] - Session tag for multi-session management.
|
|
204
|
+
* @param {string} [opts.clientId] - Client credentials ID.
|
|
205
|
+
* @param {string} [opts.clientSecret] - Client credentials secret.
|
|
206
|
+
* @param {string} [opts.username] - Username for password-based login (NSS or CSS).
|
|
207
|
+
* @param {string} [opts.password] - Password for password-based login.
|
|
208
|
+
* @param {string} [opts.clientName='Solid App'] - Application name for dynamic registration.
|
|
209
|
+
* @param {number} [opts.port] - Local port for the interactive callback server.
|
|
210
|
+
* @param {function} [opts.openUrl] - Custom function to open the browser (receives auth URL).
|
|
211
|
+
* @returns {Promise<{webId: string, isLoggedIn: boolean, fetch: function}>}
|
|
212
|
+
*/
|
|
213
|
+
async login(opts = {}) {
|
|
214
|
+
if (opts.clientId && opts.clientSecret) {
|
|
215
|
+
return this._loginCredentials(opts);
|
|
216
|
+
}
|
|
217
|
+
const restored = await this._tryRefresh(opts.oidcIssuer, opts.tag || 'default');
|
|
218
|
+
if (restored) return restored;
|
|
219
|
+
if (opts.username && opts.password) {
|
|
220
|
+
const result = await this._loginPassword(opts);
|
|
221
|
+
if (result) return result;
|
|
222
|
+
}
|
|
223
|
+
return this._loginInteractive(opts);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async _detectServerType(origin) {
|
|
227
|
+
try {
|
|
228
|
+
const resp = await globalThis.fetch(origin, { method: 'HEAD' });
|
|
229
|
+
const powered = (resp.headers.get('x-powered-by') || '').toLowerCase();
|
|
230
|
+
if (powered.includes('community solid server')) console.log('Server uses CSS');
|
|
231
|
+
if (powered.includes('community solid server')) return 'css';
|
|
232
|
+
if (powered.includes('solid')) console.log('Server uses NSS');
|
|
233
|
+
if (powered.includes('solid')) return 'nss';
|
|
234
|
+
} catch {}
|
|
235
|
+
try {
|
|
236
|
+
const resp = await globalThis.fetch(`${origin}/.account/`, { method: 'GET' });
|
|
237
|
+
if (resp.ok || resp.status === 401) return 'css';
|
|
238
|
+
} catch {}
|
|
239
|
+
return 'unknown';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async _loginPassword({ tag = 'default', oidcIssuer, username, password } = {}) {
|
|
243
|
+
if (!oidcIssuer) throw new Error('oidcIssuer is required');
|
|
244
|
+
const origin = oidcIssuer.replace(/\/$/, '');
|
|
245
|
+
const serverType = await this._detectServerType(origin);
|
|
246
|
+
|
|
247
|
+
if (serverType === 'nss') {
|
|
248
|
+
const result = await this._loginPasswordNSS({ tag, oidcIssuer, origin, username, password });
|
|
249
|
+
if (result) return result;
|
|
250
|
+
} else {
|
|
251
|
+
const result = await this._loginPasswordCSS({ tag, oidcIssuer, origin, username, password });
|
|
252
|
+
if (result) return result;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
console.warn('sol-login-node: password login failed, falling back to browser');
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async _loginPasswordNSS({ tag, oidcIssuer, origin, username, password }) {
|
|
260
|
+
try {
|
|
261
|
+
const resp = await globalThis.fetch(`${origin}/login/password`, {
|
|
262
|
+
method: 'POST',
|
|
263
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
264
|
+
body: `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`,
|
|
265
|
+
redirect: 'manual',
|
|
266
|
+
});
|
|
267
|
+
if (resp.status === 400) {
|
|
268
|
+
console.warn('sol-login-node: NSS login returned 400 (bad credentials)');
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
const cookies = typeof resp.headers.getSetCookie === 'function'
|
|
272
|
+
? resp.headers.getSetCookie()
|
|
273
|
+
: (resp.headers.get('set-cookie') || '').split(/,\s*(?=[^;]+=)/);
|
|
274
|
+
const cookie = cookies.filter(Boolean).join('; ');
|
|
275
|
+
if (!cookie) {
|
|
276
|
+
console.warn(`sol-login-node: NSS login returned ${resp.status}, no cookie`);
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
const webId = `${origin}/profile/card#me`;
|
|
280
|
+
const session = {
|
|
281
|
+
info: { isLoggedIn: true, webId, issuer: oidcIssuer },
|
|
282
|
+
fetch: (input, init = {}) => {
|
|
283
|
+
const headers = new Headers(init.headers);
|
|
284
|
+
headers.set('Cookie', cookie);
|
|
285
|
+
return globalThis.fetch(input, { ...init, headers });
|
|
286
|
+
},
|
|
287
|
+
async logout() { this.info.isLoggedIn = false; this.info.webId = null; },
|
|
288
|
+
};
|
|
289
|
+
this.sessions.set(tag, session);
|
|
290
|
+
return this._makeSessionResult(session);
|
|
291
|
+
} catch (e) {
|
|
292
|
+
console.warn(`sol-login-node: NSS login failed: ${e.message}`);
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async _loginPasswordCSS({ tag, oidcIssuer, origin, username, password }) {
|
|
298
|
+
// CSS v7+: login, discover client-credentials endpoint, create credentials
|
|
299
|
+
try {
|
|
300
|
+
const loginResp = await globalThis.fetch(`${origin}/.account/login/password/`, {
|
|
301
|
+
method: 'POST',
|
|
302
|
+
headers: { 'Content-Type': 'application/json' },
|
|
303
|
+
body: JSON.stringify({ email: username, password }),
|
|
304
|
+
});
|
|
305
|
+
if (loginResp.ok) {
|
|
306
|
+
const { authorization } = await loginResp.json();
|
|
307
|
+
const authHeader = { 'Authorization': `CSS-Account-Token ${authorization}` };
|
|
308
|
+
const indexResp = await globalThis.fetch(`${origin}/.account/`, { headers: authHeader });
|
|
309
|
+
const { controls } = await indexResp.json();
|
|
310
|
+
const credUrl = controls?.account?.clientCredentials;
|
|
311
|
+
const webIdUrl = controls?.account?.webId;
|
|
312
|
+
let webId = `${origin}/profile/card#me`;
|
|
313
|
+
if (webIdUrl) {
|
|
314
|
+
try {
|
|
315
|
+
const wResp = await globalThis.fetch(webIdUrl, { headers: authHeader });
|
|
316
|
+
const wData = await wResp.json();
|
|
317
|
+
if (wData.webIdLinks && Object.keys(wData.webIdLinks).length > 0) {
|
|
318
|
+
webId = Object.keys(wData.webIdLinks)[0];
|
|
319
|
+
}
|
|
320
|
+
} catch {}
|
|
321
|
+
}
|
|
322
|
+
if (credUrl) {
|
|
323
|
+
const credResp = await globalThis.fetch(credUrl, {
|
|
324
|
+
method: 'POST',
|
|
325
|
+
headers: { ...authHeader, 'Content-Type': 'application/json' },
|
|
326
|
+
body: JSON.stringify({ name: 'sol-login-node', webId }),
|
|
327
|
+
});
|
|
328
|
+
if (credResp.ok) {
|
|
329
|
+
const { id, secret } = await credResp.json();
|
|
330
|
+
return this._loginCredentials({ tag, oidcIssuer, clientId: id, clientSecret: secret });
|
|
331
|
+
}
|
|
332
|
+
console.warn(`sol-login-node: CSS v7 credentials returned ${credResp.status}`);
|
|
333
|
+
} else {
|
|
334
|
+
console.warn('sol-login-node: CSS v7 no clientCredentials endpoint found');
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
console.warn(`sol-login-node: CSS v7 login returned ${loginResp.status}`);
|
|
338
|
+
}
|
|
339
|
+
} catch (e) {
|
|
340
|
+
console.warn(`sol-login-node: CSS v7 login failed: ${e.message}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Older CSS: POST /idp/credentials/
|
|
344
|
+
try {
|
|
345
|
+
const credResp = await globalThis.fetch(`${origin}/idp/credentials/`, {
|
|
346
|
+
method: 'POST',
|
|
347
|
+
headers: { 'Content-Type': 'application/json' },
|
|
348
|
+
body: JSON.stringify({ email: username, password, name: 'sol-login-node' }),
|
|
349
|
+
});
|
|
350
|
+
if (credResp.ok) {
|
|
351
|
+
const { id, secret } = await credResp.json();
|
|
352
|
+
return this._loginCredentials({ tag, oidcIssuer, clientId: id, clientSecret: secret });
|
|
353
|
+
}
|
|
354
|
+
console.warn(`sol-login-node: CSS legacy credentials returned ${credResp.status}`);
|
|
355
|
+
} catch (e) {
|
|
356
|
+
console.warn(`sol-login-node: CSS legacy login failed: ${e.message}`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async _loginCredentials({ tag = 'default', oidcIssuer, clientId, clientSecret, clientName = 'Solid App' } = {}) {
|
|
363
|
+
if (!oidcIssuer) throw new Error('oidcIssuer is required');
|
|
364
|
+
if (!clientId || !clientSecret) throw new Error('clientId and clientSecret are required');
|
|
365
|
+
|
|
366
|
+
const SessionClass = await getSessionClass();
|
|
367
|
+
const session = new SessionClass();
|
|
368
|
+
|
|
369
|
+
await session.login({
|
|
370
|
+
oidcIssuer,
|
|
371
|
+
clientId,
|
|
372
|
+
clientSecret,
|
|
373
|
+
clientName,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
if (!session.info.isLoggedIn) {
|
|
377
|
+
throw new Error(`Login failed for issuer ${oidcIssuer}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
this.sessions.set(tag, session);
|
|
381
|
+
return this._makeSessionResult(session);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
_makeSession(tag, oidcIssuer, webId, accessToken) {
|
|
385
|
+
const self = this;
|
|
386
|
+
const session = {
|
|
387
|
+
info: { isLoggedIn: true, webId, issuer: oidcIssuer },
|
|
388
|
+
fetch: (input, init = {}) => {
|
|
389
|
+
const headers = new Headers(init.headers);
|
|
390
|
+
headers.set('Authorization', `Bearer ${accessToken}`);
|
|
391
|
+
return globalThis.fetch(input, { ...init, headers });
|
|
392
|
+
},
|
|
393
|
+
async logout() {
|
|
394
|
+
this.info.isLoggedIn = false;
|
|
395
|
+
this.info.webId = null;
|
|
396
|
+
await self._clearSavedTokens(oidcIssuer);
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
this.sessions.set(tag, session);
|
|
400
|
+
return session;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async _saveTokens(oidcIssuer, tokenData) {
|
|
404
|
+
if (!this._tokenStore) return;
|
|
405
|
+
const store = await _readTokenStore(this._tokenStore);
|
|
406
|
+
store[oidcIssuer] = tokenData;
|
|
407
|
+
await _writeTokenStore(this._tokenStore, store);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async _clearSavedTokens(oidcIssuer) {
|
|
411
|
+
if (!this._tokenStore) return;
|
|
412
|
+
const store = await _readTokenStore(this._tokenStore);
|
|
413
|
+
delete store[oidcIssuer];
|
|
414
|
+
await _writeTokenStore(this._tokenStore, store);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async _tryRefresh(oidcIssuer, tag) {
|
|
418
|
+
if (!this._tokenStore) return null;
|
|
419
|
+
const store = await _readTokenStore(this._tokenStore);
|
|
420
|
+
const saved = store[oidcIssuer];
|
|
421
|
+
if (!saved?.refresh_token || !saved?.client_id) return null;
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
const { Issuer } = await import('openid-client');
|
|
425
|
+
const issuer = await Issuer.discover(oidcIssuer);
|
|
426
|
+
const client = new issuer.Client({
|
|
427
|
+
client_id: saved.client_id,
|
|
428
|
+
client_secret: saved.client_secret,
|
|
429
|
+
token_endpoint_auth_method: saved.client_secret ? 'client_secret_basic' : 'none',
|
|
430
|
+
});
|
|
431
|
+
const tokenSet = await client.refresh(saved.refresh_token);
|
|
432
|
+
const claims = tokenSet.claims();
|
|
433
|
+
const webId = claims.webid || claims.sub || saved.webId;
|
|
434
|
+
|
|
435
|
+
await this._saveTokens(oidcIssuer, {
|
|
436
|
+
...saved,
|
|
437
|
+
access_token: tokenSet.access_token,
|
|
438
|
+
refresh_token: tokenSet.refresh_token || saved.refresh_token,
|
|
439
|
+
webId,
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const session = this._makeSession(tag, oidcIssuer, webId, tokenSet.access_token);
|
|
443
|
+
return this._makeSessionResult(session);
|
|
444
|
+
} catch {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async _loginInteractive({ tag = 'default', oidcIssuer, clientName = 'Solid App', port, openUrl } = {}) {
|
|
450
|
+
if (!oidcIssuer) throw new Error('oidcIssuer is required');
|
|
451
|
+
|
|
452
|
+
const { Issuer, generators } = await import('openid-client');
|
|
453
|
+
const { server, port: actualPort } = await _startCallbackServer(port || 0);
|
|
454
|
+
const redirectUrl = `http://localhost:${actualPort}/callback`;
|
|
455
|
+
|
|
456
|
+
const issuer = await Issuer.discover(oidcIssuer);
|
|
457
|
+
const registered = await issuer.Client.register({
|
|
458
|
+
redirect_uris: [redirectUrl],
|
|
459
|
+
client_name: clientName,
|
|
460
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
|
461
|
+
response_types: ['code'],
|
|
462
|
+
scope: 'openid webid offline_access',
|
|
463
|
+
token_endpoint_auth_method: 'none',
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const codeVerifier = generators.codeVerifier();
|
|
467
|
+
const codeChallenge = generators.codeChallenge(codeVerifier);
|
|
468
|
+
const state = generators.state();
|
|
469
|
+
|
|
470
|
+
const authUrl = registered.authorizationUrl({
|
|
471
|
+
redirect_uri: redirectUrl,
|
|
472
|
+
scope: 'openid webid offline_access',
|
|
473
|
+
code_challenge: codeChallenge,
|
|
474
|
+
code_challenge_method: 'S256',
|
|
475
|
+
state,
|
|
476
|
+
response_type: 'code',
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const loginComplete = new Promise((resolve, reject) => {
|
|
480
|
+
const cleanup = () => { clearTimeout(timeout); server.close(); };
|
|
481
|
+
|
|
482
|
+
const timeout = setTimeout(() => {
|
|
483
|
+
cleanup();
|
|
484
|
+
reject(new Error('Interactive login timed out (5 minutes)'));
|
|
485
|
+
}, 5 * 60 * 1000);
|
|
486
|
+
|
|
487
|
+
const onInterrupt = () => { cleanup(); reject(new Error('Login cancelled')); };
|
|
488
|
+
process.once('SIGINT', onInterrupt);
|
|
489
|
+
|
|
490
|
+
server.on('request', async (req, res) => {
|
|
491
|
+
if (!req.url.startsWith('/callback')) {
|
|
492
|
+
res.writeHead(404);
|
|
493
|
+
res.end();
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
const callbackUrl = new URL(`http://localhost:${actualPort}${req.url}`);
|
|
498
|
+
const error = callbackUrl.searchParams.get('error');
|
|
499
|
+
if (error) {
|
|
500
|
+
const desc = callbackUrl.searchParams.get('error_description') || error;
|
|
501
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
502
|
+
res.end(`<html><body><h2>Login cancelled</h2><p>${desc}</p></body></html>`);
|
|
503
|
+
process.removeListener('SIGINT', onInterrupt);
|
|
504
|
+
cleanup();
|
|
505
|
+
reject(new Error(`Login denied: ${desc}`));
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const params = registered.callbackParams(callbackUrl.href);
|
|
510
|
+
const tokenSet = await registered.callback(redirectUrl, params, {
|
|
511
|
+
code_verifier: codeVerifier,
|
|
512
|
+
state,
|
|
513
|
+
});
|
|
514
|
+
const claims = tokenSet.claims();
|
|
515
|
+
const webId = claims.webid || claims.sub;
|
|
516
|
+
|
|
517
|
+
if (!tokenSet.refresh_token) {
|
|
518
|
+
console.warn('sol-login-node: no refresh_token received — session will not persist across restarts');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
await this._saveTokens(oidcIssuer, {
|
|
522
|
+
access_token: tokenSet.access_token,
|
|
523
|
+
refresh_token: tokenSet.refresh_token,
|
|
524
|
+
client_id: registered.metadata.client_id,
|
|
525
|
+
client_secret: registered.metadata.client_secret,
|
|
526
|
+
webId,
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
const session = this._makeSession(tag, oidcIssuer, webId, tokenSet.access_token);
|
|
530
|
+
|
|
531
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
532
|
+
res.end('<html><body><h2>Login successful!</h2><p>You can close this tab.</p></body></html>');
|
|
533
|
+
process.removeListener('SIGINT', onInterrupt);
|
|
534
|
+
cleanup();
|
|
535
|
+
resolve(this._makeSessionResult(session));
|
|
536
|
+
} catch (e) {
|
|
537
|
+
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
538
|
+
res.end(`<html><body><h2>Login failed</h2><p>${e.message}</p></body></html>`);
|
|
539
|
+
process.removeListener('SIGINT', onInterrupt);
|
|
540
|
+
cleanup();
|
|
541
|
+
reject(e);
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
const opener = openUrl || _openBrowser;
|
|
547
|
+
await opener(authUrl);
|
|
548
|
+
|
|
549
|
+
return loginComplete;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Manually register an external session (e.g. one obtained outside SolidAuth).
|
|
554
|
+
* @param {string} tag - Session tag.
|
|
555
|
+
* @param {Object} session - A session object with `info` and `fetch`.
|
|
556
|
+
*/
|
|
557
|
+
addSession(tag, session) {
|
|
558
|
+
this.sessions.set(tag, session);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Find the best session for a URL. Checks the tagged session first,
|
|
563
|
+
* then falls back to any session whose issuer/webId covers the URL's origin.
|
|
564
|
+
* @param {string} url
|
|
565
|
+
* @param {string} [tag]
|
|
566
|
+
* @returns {Object|null} The matching session, or null.
|
|
567
|
+
*/
|
|
568
|
+
getSessionFor(url, tag) {
|
|
569
|
+
return _getSessionFor(this.sessions, url, tag, this._noAuth);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Get an authenticated fetch function for a URL. Returns `globalThis.fetch`
|
|
574
|
+
* if the URL is in the noAuth list or no matching session is found.
|
|
575
|
+
* @param {string} url
|
|
576
|
+
* @param {string} [tag] - Preferred session tag.
|
|
577
|
+
* @returns {function} A fetch function (possibly authenticated).
|
|
578
|
+
*/
|
|
579
|
+
fetchFor(url, tag) {
|
|
580
|
+
return makeFetchFor(this.sessions, url, tag, this._noAuth, globalThis.fetch);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Check whether there is an active session that covers the given URL.
|
|
585
|
+
* Returns true for noAuth URLs even without a session.
|
|
586
|
+
* @param {string} url
|
|
587
|
+
* @param {string} [tag]
|
|
588
|
+
* @returns {boolean}
|
|
589
|
+
*/
|
|
590
|
+
isLoggedIn(url, tag) {
|
|
591
|
+
return isLoggedInFor(this.sessions, url, tag, this._noAuth);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Get the WebID for a tagged session.
|
|
596
|
+
* @param {string} [tag='default']
|
|
597
|
+
* @returns {string|null}
|
|
598
|
+
*/
|
|
599
|
+
getWebId(tag = 'default') {
|
|
600
|
+
return _getWebId(this.sessions, tag);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Get the first session that is currently logged in.
|
|
605
|
+
* @returns {Object|null} The session, or null if none are active.
|
|
606
|
+
*/
|
|
607
|
+
getFirstLoggedIn() {
|
|
608
|
+
return _getFirstLoggedIn(this.sessions);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Log out one or all sessions. If `tag` is given, only that session is
|
|
613
|
+
* removed; otherwise all sessions are logged out and cleared.
|
|
614
|
+
* @param {string} [tag] - Session tag to log out. Omit to log out all.
|
|
615
|
+
*/
|
|
616
|
+
async logout(tag) {
|
|
617
|
+
if (tag) {
|
|
618
|
+
const s = this.sessions.get(tag);
|
|
619
|
+
if (s) {
|
|
620
|
+
if (s.info?.isLoggedIn) await s.logout();
|
|
621
|
+
this.sessions.delete(tag);
|
|
622
|
+
}
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
for (const [, s] of this.sessions) {
|
|
626
|
+
if (s.info?.isLoggedIn) await s.logout();
|
|
627
|
+
}
|
|
628
|
+
this.sessions.clear();
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export default SolidAuth;
|