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.
Files changed (150) hide show
  1. package/README.md +7 -0
  2. package/core/activate.js +27 -0
  3. package/core/adopt.js +71 -0
  4. package/core/auth-core.js +73 -0
  5. package/core/auth-fetch.js +154 -0
  6. package/core/component-mount.js +110 -0
  7. package/core/defaults.js +48 -0
  8. package/core/define.js +15 -0
  9. package/core/display-target.js +166 -0
  10. package/core/edit-placements.js +28 -0
  11. package/core/editor-self.js +127 -0
  12. package/core/editor.js +162 -0
  13. package/core/events.js +27 -0
  14. package/core/extension-points.js +189 -0
  15. package/core/form-utils.js +210 -0
  16. package/core/from-query.js +138 -0
  17. package/core/from-rdf.js +52 -0
  18. package/core/here.js +33 -0
  19. package/core/include-core.js +73 -0
  20. package/core/inrupt-global.js +18 -0
  21. package/core/menu-consumer.js +41 -0
  22. package/core/menu-rdf.js +154 -0
  23. package/core/pod-ops.js +392 -0
  24. package/core/pod-registry.js +82 -0
  25. package/core/popup-proxy.js +255 -0
  26. package/core/rdf-core.js +280 -0
  27. package/core/rdf-render.js +136 -0
  28. package/core/rdf-utils.js +411 -0
  29. package/core/rdf.js +154 -0
  30. package/core/services.js +106 -0
  31. package/core/shape-to-form.js +741 -0
  32. package/core/sparql-safety.js +20 -0
  33. package/core/utils.js +196 -0
  34. package/dist/importmap-cdn.json +49 -0
  35. package/dist/importmap-local.json +49 -0
  36. package/dist/sol-loader.manifest.json +140 -0
  37. package/dist/vendor/@comunica-query-sparql.js +137851 -0
  38. package/dist/vendor/@inrupt-solid-client-authn-browser.js +7503 -0
  39. package/dist/vendor/dompurify.js +1476 -0
  40. package/dist/vendor/ical.js.js +9739 -0
  41. package/dist/vendor/marked.js +85 -0
  42. package/dist/vendor/n3.js +14670 -0
  43. package/dist/vendor/rdf-validate-shacl.js +6970 -0
  44. package/dist/vendor/rdflib.js +35172 -0
  45. package/dist/vendor/solid-logic.js +6819 -0
  46. package/dist/vendor/solid-ui.js +21945 -0
  47. package/node/sol-form.js +133 -0
  48. package/node/sol-include.js +55 -0
  49. package/node/sol-login.js +632 -0
  50. package/node/sol-menu.js +639 -0
  51. package/node/sol-query.js +116 -0
  52. package/package.json +133 -0
  53. package/web/menu-from-rdf.js +23 -0
  54. package/web/scripts/prefs.js +25 -0
  55. package/web/sol-accordion.js +114 -0
  56. package/web/sol-basic.js +50 -0
  57. package/web/sol-breadcrumb.js +131 -0
  58. package/web/sol-button.js +244 -0
  59. package/web/sol-calendar.js +465 -0
  60. package/web/sol-default.js +118 -0
  61. package/web/sol-dropdown-button.js +222 -0
  62. package/web/sol-feed.js +1336 -0
  63. package/web/sol-form.js +949 -0
  64. package/web/sol-full.js +43 -0
  65. package/web/sol-gallery.js +303 -0
  66. package/web/sol-include.js +246 -0
  67. package/web/sol-live-edit.js +415 -0
  68. package/web/sol-login.js +856 -0
  69. package/web/sol-menu.js +593 -0
  70. package/web/sol-modal.js +377 -0
  71. package/web/sol-pod-extras.js +17 -0
  72. package/web/sol-pod-ops.js +680 -0
  73. package/web/sol-pod.js +1039 -0
  74. package/web/sol-query.js +546 -0
  75. package/web/sol-rolodex.js +95 -0
  76. package/web/sol-search.js +402 -0
  77. package/web/sol-settings.js +199 -0
  78. package/web/sol-solidos.js +93 -0
  79. package/web/sol-tabs.js +445 -0
  80. package/web/sol-time.js +194 -0
  81. package/web/sol-tree-edit.js +492 -0
  82. package/web/sol-wac.js +456 -0
  83. package/web/sol-weather.js +337 -0
  84. package/web/sol-window.js +142 -0
  85. package/web/styles/buttons-css.js +108 -0
  86. package/web/styles/help.css +242 -0
  87. package/web/styles/root.css +112 -0
  88. package/web/styles/sol-accordion-css.js +97 -0
  89. package/web/styles/sol-calendar-css.js +154 -0
  90. package/web/styles/sol-feed-css.js +475 -0
  91. package/web/styles/sol-form-css.js +471 -0
  92. package/web/styles/sol-gallery-css.js +181 -0
  93. package/web/styles/sol-include-css.js +95 -0
  94. package/web/styles/sol-live-edit-css.js +84 -0
  95. package/web/styles/sol-live-edit.css +101 -0
  96. package/web/styles/sol-login-css.js +116 -0
  97. package/web/styles/sol-menu-css.js +145 -0
  98. package/web/styles/sol-modal-css.js +134 -0
  99. package/web/styles/sol-pod-css.js +187 -0
  100. package/web/styles/sol-pod-modal-css.js +203 -0
  101. package/web/styles/sol-query-css.js +140 -0
  102. package/web/styles/sol-query-help.css +267 -0
  103. package/web/styles/sol-query-one-pager.css +67 -0
  104. package/web/styles/sol-search-css.js +157 -0
  105. package/web/styles/sol-solidos-css.js +7 -0
  106. package/web/styles/sol-tabs-css.js +114 -0
  107. package/web/styles/sol-time-css.js +30 -0
  108. package/web/styles/sol-wac-css.js +73 -0
  109. package/web/styles/sol-weather-css.js +59 -0
  110. package/web/styles/solid-logo.svg +9 -0
  111. package/web/styles/view-accordion-css.js +66 -0
  112. package/web/styles/view-anchorlist-css.js +22 -0
  113. package/web/styles/view-autocomplete-css.js +59 -0
  114. package/web/styles/view-rolodex-css.js +102 -0
  115. package/web/styles/view-select-css.js +21 -0
  116. package/web/utils/calendar-fetch.js +388 -0
  117. package/web/utils/code-mirror-editor.js +82 -0
  118. package/web/utils/commons-fetch.js +108 -0
  119. package/web/utils/feed-edit.js +159 -0
  120. package/web/utils/feed-edit.smoke.mjs +74 -0
  121. package/web/utils/feed-fetch.js +573 -0
  122. package/web/utils/live-edit-help/csv.js +64 -0
  123. package/web/utils/live-edit-help/graphviz.js +41 -0
  124. package/web/utils/live-edit-help/jsonld.js +55 -0
  125. package/web/utils/live-edit-help/markdown.js +52 -0
  126. package/web/utils/live-edit-help/mermaid.js +48 -0
  127. package/web/utils/live-edit-help/turtle.js +85 -0
  128. package/web/utils/rdf-config.js +125 -0
  129. package/web/utils/renderers/csv.js +124 -0
  130. package/web/utils/renderers/d3-force.js +82 -0
  131. package/web/utils/renderers/graphviz.js +13 -0
  132. package/web/utils/renderers/html.js +10 -0
  133. package/web/utils/renderers/jsonld.js +63 -0
  134. package/web/utils/renderers/markdown.js +19 -0
  135. package/web/utils/renderers/mermaid.js +54 -0
  136. package/web/utils/renderers/turtle.js +51 -0
  137. package/web/utils/sol-query-triple-patterns.js +151 -0
  138. package/web/utils/sol-query-ui.js +250 -0
  139. package/web/utils/sol-query-views.js +32 -0
  140. package/web/views/_helpers.js +34 -0
  141. package/web/views/accordion.js +133 -0
  142. package/web/views/anchorlist.js +59 -0
  143. package/web/views/auto-complete.js +183 -0
  144. package/web/views/dl.js +38 -0
  145. package/web/views/list.js +19 -0
  146. package/web/views/menu.js +56 -0
  147. package/web/views/rolodex.js +126 -0
  148. package/web/views/select.js +79 -0
  149. package/web/views/table.js +73 -0
  150. package/web/views/tabs.js +57 -0
@@ -0,0 +1,856 @@
1
+ /**
2
+ * <sol-login> — Solid OIDC login web component.
3
+ * Attributes: issuers (comma-separated list of known issuer origins)
4
+ * Properties: fetchFor(url) — authenticated fetch, webId, isLoggedIn, session
5
+ * Events: sol-login({webId, issuer}), sol-logout
6
+ *
7
+ * Usage:
8
+ * <sol-login></sol-login>
9
+ * <sol-login issuers="https://solidcommunity.net,https://login.inrupt.com"></sol-login>
10
+ *
11
+ * Expects @inrupt/solid-client-authn-browser loaded as UMD at window.solidClientAuthn
12
+ */
13
+
14
+ import { CSS, sheet as LOGIN_SHEET } from './styles/sol-login-css.js';
15
+ import { adopt } from '../core/adopt.js';
16
+ import { define } from '../core/define.js';
17
+ import { rdf } from '../core/rdf.js';
18
+ import {
19
+ originOf,
20
+ baseDomain,
21
+ sessionCoversOrigin,
22
+ isNoAuth as _isNoAuth,
23
+ getSessionFor as _getSessionFor,
24
+ makeFetchFor,
25
+ isLoggedInFor,
26
+ getWebId as _getWebId,
27
+ getFirstLoggedIn as _getFirstLoggedIn,
28
+ } from '../core/auth-core.js';
29
+ import { PopupProxySession } from '../core/popup-proxy.js';
30
+ import { solFetch } from '../core/auth-fetch.js';
31
+ import { register as registerService, root as swcRoot } from '../core/services.js';
32
+
33
+ document.addEventListener('DOMContentLoaded', async () => {
34
+ const login = document.querySelector('sol-login');
35
+ if (login && !login._manualInit) await login.initialize();
36
+ });
37
+
38
+
39
+ function getSessionClass() {
40
+ const locations = [
41
+ window.solidClientAuthn?.Session,
42
+ window.solidClientAuthentication?.Session,
43
+ window.SolidClientAuthn?.Session,
44
+ window['@inrupt/solid-client-authn-browser']?.Session
45
+ ];
46
+
47
+ for (const SessionClass of locations) {
48
+ if (SessionClass) return SessionClass;
49
+ }
50
+
51
+ throw new Error('sol-login: solid-client-authn-browser must be loaded as UMD bundle. Expected at window.solidClientAuthn.Session or window.solidClientAuthentication.Session');
52
+ }
53
+
54
+ class AuthManager {
55
+ /** The page-wide singleton. Every `<sol-login>` instance binds to
56
+ * this same AuthManager so sessions established by any embedded app
57
+ * (podz left/right, future apps) are visible to shell-level code
58
+ * without DOM probing. */
59
+ static get shared() { return sharedAuth; }
60
+
61
+ constructor() {
62
+ this.sessions = new Map();
63
+ this._noAuth = null;
64
+ try {
65
+ this._sideOrigins = JSON.parse(localStorage.getItem('solLoginOrigins') || '{}');
66
+ } catch (e) { this._sideOrigins = {}; }
67
+ }
68
+
69
+ set noAuth(v) {
70
+ this._noAuth = v;
71
+ }
72
+
73
+ _noAuthConfig() {
74
+ return this._noAuth ?? window.SolidAppContext?.noAuth;
75
+ }
76
+
77
+ isNoAuth(url) {
78
+ return _isNoAuth(url, this._noAuthConfig());
79
+ }
80
+
81
+ originOf(url) { return originOf(url); }
82
+
83
+ _sessionId(tag, origin) {
84
+ return `sol_${tag}_${origin.replace(/[^a-z0-9]/gi, '_')}`;
85
+ }
86
+
87
+ _makeSession(sessionId) {
88
+ const SessionClass = getSessionClass();
89
+ return new SessionClass({}, sessionId);
90
+ }
91
+
92
+ sessionFor(tag, origin) {
93
+ if (this.sessions.has(tag)) return this.sessions.get(tag);
94
+ const org = origin || this._sideOrigins[tag];
95
+ const sessionId = org ? this._sessionId(tag, org) : `sol_${tag}_unset`;
96
+ const session = this._makeSession(sessionId);
97
+ this.sessions.set(tag, session);
98
+ return session;
99
+ }
100
+
101
+ setSideOrigin(tag, url) {
102
+ if (this.isNoAuth(url)) return;
103
+ const origin = this.originOf(url);
104
+ if (this._sideOrigins[tag] === origin) return;
105
+ const existing = this.sessions.get(tag);
106
+ if (existing && this._sessionCoversOrigin(existing, origin)) {
107
+ this._sideOrigins[tag] = origin;
108
+ this._persistOrigins();
109
+ return;
110
+ }
111
+ this._sideOrigins[tag] = origin;
112
+ this._persistOrigins();
113
+ const sessionId = this._sessionId(tag, origin);
114
+ this.sessions.set(tag, this._makeSession(sessionId));
115
+ }
116
+
117
+ _persistOrigins() {
118
+ try { localStorage.setItem('solLoginOrigins', JSON.stringify(this._sideOrigins)); } catch (e) {}
119
+ }
120
+
121
+ _sessionCoversOrigin(session, origin) {
122
+ return sessionCoversOrigin(session, origin);
123
+ }
124
+
125
+ getSessionFor(url, tag) {
126
+ return _getSessionFor(this.sessions, url, tag, this._noAuthConfig());
127
+ }
128
+
129
+ fetchFor(url, tag) {
130
+ return makeFetchFor(this.sessions, url, tag, this._noAuthConfig(), fetch);
131
+ }
132
+
133
+ isLoggedIn(url, tag) {
134
+ return isLoggedInFor(this.sessions, url, tag, this._noAuthConfig());
135
+ }
136
+
137
+ getWebId(tag) {
138
+ return _getWebId(this.sessions, tag);
139
+ }
140
+
141
+ getFirstLoggedIn() {
142
+ return _getFirstLoggedIn(this.sessions);
143
+ }
144
+
145
+ async handleIncomingRedirect() {
146
+ const pendingTag = localStorage.getItem('solLoginPendingTag');
147
+ localStorage.removeItem('solLoginPendingTag');
148
+
149
+ // Ensure the session that initiated login exists so it can process the redirect.
150
+ if (pendingTag) {
151
+ this.sessionFor(pendingTag);
152
+ }
153
+
154
+ for (const [, session] of this.sessions) {
155
+ await session.handleIncomingRedirect(window.location.href);
156
+ }
157
+ }
158
+
159
+ async ensureAuthenticated(url, tag = 'default') {
160
+ if (this.isNoAuth(url)) return true;
161
+ const origin = this.originOf(url);
162
+ this.setSideOrigin(tag, url);
163
+ const session = this.sessionFor(tag, origin);
164
+ if (session.info.isLoggedIn) return true;
165
+
166
+ for (const [, s] of this.sessions) {
167
+ if (sessionCoversOrigin(s, origin)) return true;
168
+ }
169
+
170
+ try { localStorage.setItem('solLoginPendingTag', tag); } catch (e) {}
171
+ const redirectUrl = window.location.origin + window.location.pathname;
172
+ await session.login({ oidcIssuer: origin, redirectUrl, clientName: 'Solid App' });
173
+ return false;
174
+ }
175
+ }
176
+
177
+ // All <sol-login> instances on a page share one AuthManager so that
178
+ // podz's per-side login elements register into a single session Map.
179
+ // Single-login pages are unaffected (one consumer of the singleton).
180
+ const sharedAuth = new AuthManager();
181
+
182
+ // Publish auth to the ecosystem: expose AuthManager on the global (consumers in
183
+ // core/auth-fetch.js + web/sol-include.js already READ window.SolidWebComponents
184
+ // .AuthManager but nothing assigned it — this closes that gap), and register the
185
+ // `auth` host-service so any component reaches the signed-in fetch via
186
+ // window.SolidWebComponents.{auth,fetch} without importing swc.
187
+ try {
188
+ swcRoot().AuthManager = AuthManager;
189
+ registerService('auth', {
190
+ manager: sharedAuth,
191
+ fetch: solFetch,
192
+ fetchFor: (url, tag) => sharedAuth.fetchFor(url, tag),
193
+ });
194
+ } catch (_) { /* no window / pre-registration race — harmless */ }
195
+
196
+ /**
197
+ * Solid OIDC login web component.
198
+ *
199
+ * Shows a log-in/log-out button with issuer dropdown. Manages OIDC sessions
200
+ * via @inrupt/solid-client-authn-browser and provides authenticated fetch.
201
+ *
202
+ * Two modes (the `mode` attribute):
203
+ * - "redirect" (default) — classic full-page OIDC redirect. One session
204
+ * per page. Unchanged behavior for existing consumers.
205
+ * - "popup" — login happens in a popup window that holds the real
206
+ * Session; the parent talks to it via a PopupProxySession. Lets
207
+ * multiple <sol-login side="..."> elements hold independent sessions
208
+ * in one tab. See core/popup-proxy.js and popup-auth-callback.html.
209
+ *
210
+ * @class SolLogin
211
+ * @extends HTMLElement
212
+ * @attr {string} issuers - comma-separated list of known OIDC issuer origins
213
+ * @attr {string} mode - "redirect" (default) | "popup"
214
+ * @attr {string} side - session tag for this element (popup mode); default "default"
215
+ * @attr {string} popup-callback - URL of the popup callback page (popup mode)
216
+ * @attr {string} external-auth - set automatically when another same-origin
217
+ * window/tab/iframe reports a login over BroadcastChannel
218
+ * ('sol-auth'). Pure visual signal — CSS in sol-login-css.js
219
+ * paints the button green and surfaces the chip so the user
220
+ * can choose to also log in here. Cleared when this element
221
+ * holds its own session.
222
+ *
223
+ * Cross-window auth signaling: every sol-login opens a
224
+ * BroadcastChannel('sol-auth') in connectedCallback. Own login/logout events
225
+ * are rebroadcast on the channel; foreign login messages set the
226
+ * `external-auth` attribute (and clear it on logout) — unless this element
227
+ * already has its own logged-in session, in which case foreign signals are
228
+ * ignored. A `hello` ping on connect asks existing windows to announce
229
+ * themselves so newly-mounted elements pick up state established before
230
+ * they existed. Used by dk-solidos's iframe (broadcasts mashlib's session
231
+ * via pages/solidos-host.html) and any other same-origin sol-login.
232
+ *
233
+ * @property {Function} fetchFor - fetchFor(url) returns authenticated fetch
234
+ * @property {string} webId - logged-in user's WebID
235
+ * @property {boolean} isLoggedIn - whether a session is active
236
+ * @fires sol-login - detail: { webId, issuer, side }
237
+ * @fires sol-logout - detail: { side }
238
+ */
239
+ class SolLogin extends HTMLElement {
240
+ static get observedAttributes() { return ['issuers', 'mode', 'side', 'popup-callback']; }
241
+
242
+ constructor() {
243
+ super();
244
+ this.attachShadow({ mode: 'open' });
245
+ this._auth = sharedAuth;
246
+ this._issuers = [];
247
+ this._initialized = false;
248
+ this._mode = 'redirect';
249
+ this._side = 'default';
250
+ this._popupCallback = './popup-auth-callback.html';
251
+ this._popupWindow = null;
252
+ this._popupMsgHandler = null;
253
+ }
254
+
255
+ get auth() { return this._auth; }
256
+
257
+ /** The session for this element's side (popup mode), if any. */
258
+ _sideSession() {
259
+ return this._auth.sessions.get(this._side) || null;
260
+ }
261
+
262
+ get webId() {
263
+ const s = this._mode === 'popup'
264
+ ? this._sideSession()
265
+ : this._auth.getFirstLoggedIn();
266
+ return s?.info?.webId || null;
267
+ }
268
+
269
+ get isLoggedIn() {
270
+ if (this._mode === 'popup') {
271
+ return !!this._sideSession()?.info?.isLoggedIn;
272
+ }
273
+ return !!this._auth.getFirstLoggedIn();
274
+ }
275
+
276
+ fetchFor(url, tag) {
277
+ return this._auth.fetchFor(url, tag);
278
+ }
279
+
280
+ set issuers(arr) {
281
+ this._issuers = arr || [];
282
+ if (this.isConnected) this._renderIssuers();
283
+ }
284
+
285
+ get issuers() { return this._issuers; }
286
+
287
+ addIssuer(origin) {
288
+ try {
289
+ const o = new URL(origin).origin;
290
+ if (!this._issuers.includes(o)) {
291
+ this._issuers.push(o);
292
+ if (this.isConnected) this._renderIssuers();
293
+ }
294
+ } catch (e) {}
295
+ }
296
+
297
+ connectedCallback() {
298
+ if (!this._initialized) {
299
+ this._initialized = true;
300
+ this._mode = (this.getAttribute('mode') || 'redirect').toLowerCase();
301
+ this._side = this.getAttribute('side') || 'default';
302
+ const cb = this.getAttribute('popup-callback');
303
+ if (cb) this._popupCallback = cb;
304
+ this._render();
305
+ const attr = this.getAttribute('issuers');
306
+ if (attr) this._issuers = attr.split(',').map(s => s.trim()).filter(Boolean);
307
+ // Self-listeners broadcast our own login/logout to the
308
+ // same-origin BroadcastChannel so other windows / iframes light
309
+ // up their login button green ('external-auth' attribute).
310
+ this.addEventListener('sol-login', (e) => this._broadcastLogin(e));
311
+ this.addEventListener('sol-logout', () => this._broadcastLogout());
312
+ }
313
+ this._attachAuthNeededListener();
314
+ this._setupAuthChannel();
315
+ }
316
+
317
+ disconnectedCallback() {
318
+ if (this._popupMsgHandler) {
319
+ window.removeEventListener('message', this._popupMsgHandler);
320
+ this._popupMsgHandler = null;
321
+ }
322
+ this._detachAuthNeededListener();
323
+ if (this._authChannel) {
324
+ this._authChannel.close();
325
+ this._authChannel = null;
326
+ }
327
+ }
328
+
329
+ /* ── cross-window auth signaling ───────────────────────────────────
330
+ * BroadcastChannel('sol-auth') carries login/logout across every
331
+ * same-origin tab, window, and iframe (including mashlib in the
332
+ * dk-solidos iframe, which broadcasts via pages/solidos-host.html).
333
+ * On receipt of a foreign login, if THIS sol-login has no own
334
+ * logged-in session, we set the `external-auth` attribute — pure
335
+ * CSS in sol-login-css.js paints the button green to invite the
336
+ * user to log in here too. */
337
+ _setupAuthChannel() {
338
+ if (this._authChannel) return;
339
+ if (typeof BroadcastChannel === 'undefined') return;
340
+ try { this._authChannel = new BroadcastChannel('sol-auth'); }
341
+ catch (_) { return; }
342
+ this._authChannel.addEventListener('message', (e) => this._onAuthMessage(e));
343
+ // Ask any already-logged-in window to announce itself, so our
344
+ // newly-connected element doesn't miss state established before
345
+ // we mounted.
346
+ try { this._authChannel.postMessage({ type: 'hello' }); } catch (_) {}
347
+ }
348
+
349
+ _broadcastLogin(e) {
350
+ if (!this._authChannel) return;
351
+ try {
352
+ this._authChannel.postMessage({
353
+ type: 'login',
354
+ webId: e?.detail?.webId,
355
+ side: e?.detail?.side ?? this._side,
356
+ });
357
+ } catch (_) {}
358
+ // We're now the source; clear any prior external-auth on self.
359
+ this.removeAttribute('external-auth');
360
+ }
361
+
362
+ _broadcastLogout() {
363
+ if (!this._authChannel) return;
364
+ try { this._authChannel.postMessage({ type: 'logout' }); } catch (_) {}
365
+ }
366
+
367
+ _onAuthMessage(e) {
368
+ const d = e?.data;
369
+ if (!d || typeof d !== 'object') return;
370
+ if (d.type === 'hello') {
371
+ // Reply to newcomers if we hold a session.
372
+ const s = this._auth.getFirstLoggedIn();
373
+ if (s && this._authChannel) {
374
+ try {
375
+ this._authChannel.postMessage({
376
+ type: 'login', webId: s.info?.webId, side: this._side,
377
+ });
378
+ } catch (_) {}
379
+ }
380
+ return;
381
+ }
382
+ if (d.type === 'login') {
383
+ if (this._auth.getFirstLoggedIn()) return;
384
+ this.setAttribute('external-auth', d.webId || '');
385
+ } else if (d.type === 'logout') {
386
+ // Conservative: clear immediately. If a different window is
387
+ // still logged in, its next 'hello' / 'login' event repaints.
388
+ this.removeAttribute('external-auth');
389
+ }
390
+ }
391
+
392
+ /* ── sol-auth-needed listener ──────────────────────────────────────
393
+ * Components save through solFetch (core/auth-fetch.js); when a
394
+ * request returns 401, solFetch dispatches `sol-auth-needed` and
395
+ * waits for someone to resolve its detail promise. We listen on
396
+ * `document`, pick the default issuer (own `issuer` attribute, then
397
+ * `<sol-default default-issuer>`, then the first entry in our list),
398
+ * run the existing login flow, and resolve the promise on success or
399
+ * give-up.
400
+ *
401
+ * Concurrent prompts are coalesced — multiple solFetch callers that
402
+ * hit 401 in the same window will share one login attempt rather
403
+ * than stacking popups.
404
+ */
405
+
406
+ _attachAuthNeededListener() {
407
+ if (this._authNeededHandler) return;
408
+ this._authNeededHandler = (e) => this._handleAuthNeeded(e);
409
+ document.addEventListener('sol-auth-needed', this._authNeededHandler);
410
+ }
411
+
412
+ _detachAuthNeededListener() {
413
+ if (this._authNeededHandler) {
414
+ document.removeEventListener('sol-auth-needed', this._authNeededHandler);
415
+ this._authNeededHandler = null;
416
+ }
417
+ }
418
+
419
+ _resolveDefaultIssuer() {
420
+ return this.getAttribute('issuer')
421
+ || (document.querySelector('sol-default')?.getAttribute('default-issuer'))
422
+ || this._issuers[0]
423
+ || null;
424
+ }
425
+
426
+ async _handleAuthNeeded(e) {
427
+ const { resolve, reject, side } = e.detail || {};
428
+ if (typeof resolve !== 'function') return;
429
+
430
+ // Side-scoped routing: if the event names a side, only the matching
431
+ // <sol-login> handles it. Untagged events (no side) fall through to
432
+ // every listener — first to settle wins, others no-op. This keeps
433
+ // left/right pod chips from both popping their dropdowns for the
434
+ // same authTag-bearing 401.
435
+ if (side && side !== this._side) return;
436
+
437
+ if (this._pendingAuthPromise) {
438
+ try { resolve(await this._pendingAuthPromise); }
439
+ catch (err) { reject?.(err); }
440
+ return;
441
+ }
442
+
443
+ const issuer = this._resolveDefaultIssuer();
444
+ if (!issuer) { resolve(false); return; }
445
+
446
+ // Surface the element for the duration of the auth flow so the
447
+ // user can pick a different issuer (the picker dropdown lives in
448
+ // sol-login's own UI). `active` is the CSS hook in
449
+ // styles/sol-login-css.js — :host([active]) flips display back on.
450
+ this.setAttribute('active', '');
451
+
452
+ // Open the dropdown so the issuer list is visible while auto-login
453
+ // is running. The user can click a different issuer to switch
454
+ // (which closes the in-flight popup and opens a fresh one — see
455
+ // _popupLogin's reissue handling).
456
+ requestAnimationFrame(() => {
457
+ this._showSwitchHint(issuer);
458
+ this._toggleDropdown();
459
+ });
460
+
461
+ this._pendingAuthPromise = new Promise((res) => {
462
+ const cleanup = () => {
463
+ this.removeEventListener('sol-login', onLogin);
464
+ this.removeEventListener('sol-popup-blocked', onFail);
465
+ this.removeAttribute('active');
466
+ this._closeDropdown();
467
+ this._hideSwitchHint();
468
+ this._pendingAuthPromise = null;
469
+ };
470
+ const onLogin = () => { cleanup(); res(true); };
471
+ const onFail = () => { cleanup(); res(false); };
472
+ this.addEventListener('sol-login', onLogin);
473
+ this.addEventListener('sol-popup-blocked', onFail);
474
+ Promise.resolve(this.login(issuer)).catch(() => onFail());
475
+ });
476
+
477
+ try { resolve(await this._pendingAuthPromise); }
478
+ catch (err) { reject?.(err); }
479
+ }
480
+
481
+ attributeChangedCallback(name, oldV, newV) {
482
+ if (oldV === newV) return;
483
+ if (name === 'issuers' && this._initialized) {
484
+ this._issuers = (newV || '').split(',').map(s => s.trim()).filter(Boolean);
485
+ this._renderIssuers();
486
+ } else if (name === 'mode' && this._initialized) {
487
+ this._mode = (newV || 'redirect').toLowerCase();
488
+ } else if (name === 'side' && this._initialized) {
489
+ this._side = newV || 'default';
490
+ this._updateUI();
491
+ } else if (name === 'popup-callback' && newV) {
492
+ this._popupCallback = newV;
493
+ }
494
+ }
495
+
496
+ async login(issuerUrl, tag = 'default') {
497
+ if (this._mode === 'popup') {
498
+ return this._popupLogin(issuerUrl);
499
+ }
500
+ await this._auth.ensureAuthenticated(issuerUrl, tag);
501
+ }
502
+
503
+ /**
504
+ * Popup-mode login. Opens the callback page in a popup that runs the
505
+ * OIDC redirect on its own; when it posts back `logged-in`, we wrap the
506
+ * popup in a PopupProxySession and register it under this element's side.
507
+ */
508
+ _popupLogin(issuerUrl) {
509
+ let issuer = issuerUrl;
510
+ try { issuer = new URL(issuerUrl).href; } catch (e) {
511
+ this._setStatusMessage('Invalid issuer URL', true);
512
+ return;
513
+ }
514
+
515
+ // Reuse vs. reissue: if a popup is already open for this side and
516
+ // the issuer matches, just refocus. If the caller is switching to
517
+ // a different issuer (e.g. user clicked another option in the
518
+ // dropdown while auto-login was in flight), close the old popup
519
+ // and open a fresh one with the new issuer URL.
520
+ if (this._popupWindow && !this._popupWindow.closed) {
521
+ if (this._popupIssuer === issuer) {
522
+ this._popupWindow.focus();
523
+ return;
524
+ }
525
+ try { this._popupWindow.close(); } catch (e) {}
526
+ this._popupWindow = null;
527
+ }
528
+
529
+ const url = this._popupCallback +
530
+ (this._popupCallback.includes('?') ? '&' : '?') +
531
+ 'side=' + encodeURIComponent(this._side) +
532
+ '&issuer=' + encodeURIComponent(issuer);
533
+ const features = 'popup=yes,width=480,height=620';
534
+ const w = window.open(url, 'sol-login-' + this._side, features);
535
+ if (!w) {
536
+ this._setStatusMessage('Popup blocked — allow popups and retry', true);
537
+ this.dispatchEvent(new CustomEvent('sol-popup-blocked', {
538
+ bubbles: true, composed: true, detail: { side: this._side },
539
+ }));
540
+ return;
541
+ }
542
+ this._popupWindow = w;
543
+ this._popupIssuer = issuer;
544
+ this._setStatusMessage('Signing in…');
545
+ // Auto-login also wants the hint updated as the user re-picks.
546
+ if (this.hasAttribute('active')) this._showSwitchHint(issuer);
547
+
548
+ if (!this._popupMsgHandler) {
549
+ this._popupMsgHandler = (e) => this._onPopupMessage(e);
550
+ window.addEventListener('message', this._popupMsgHandler);
551
+ }
552
+ }
553
+
554
+ _onPopupMessage(e) {
555
+ const d = e.data;
556
+ if (!d || d.source !== 'sol-popup-auth') return;
557
+ if (d.side && d.side !== this._side) return;
558
+
559
+ if (d.type === 'logged-in') {
560
+ const proxy = new PopupProxySession(this._popupWindow, {
561
+ webId: d.webId, sessionId: d.sessionId, issuer: d.issuer,
562
+ clientId: null, side: this._side,
563
+ }, window.location.origin);
564
+ proxy.addEventListener('logout', () => {
565
+ if (this._auth.sessions.get(this._side) === proxy) {
566
+ this._auth.sessions.delete(this._side);
567
+ }
568
+ this._popupWindow = null;
569
+ this._updateUI();
570
+ this.dispatchEvent(new CustomEvent('sol-logout', {
571
+ bubbles: true, composed: true, detail: { side: this._side },
572
+ }));
573
+ });
574
+ this._auth.sessions.set(this._side, proxy);
575
+ this._updateUI();
576
+ this.dispatchEvent(new CustomEvent('sol-login', {
577
+ bubbles: true, composed: true,
578
+ detail: { webId: d.webId, issuer: d.issuer, side: this._side },
579
+ }));
580
+ this._integrateWithRdflib();
581
+ } else if (d.type === 'login-failed') {
582
+ this._popupWindow = null;
583
+ this._setStatusMessage('Sign-in failed', true);
584
+ }
585
+ }
586
+
587
+ async initialize(tags = ['default']) {
588
+ if (this._mode === 'popup') {
589
+ // PR 1: no cross-reload persistence — nothing to restore on boot.
590
+ this._updateUI();
591
+ this._integrateWithRdflib();
592
+ return;
593
+ }
594
+ for (const tag of tags) {
595
+ this._auth.sessionFor(tag);
596
+ }
597
+ await this._auth.handleIncomingRedirect();
598
+ this._updateUI();
599
+ this._integrateWithRdflib();
600
+
601
+ const firstSession = this._auth.getFirstLoggedIn();
602
+ if (firstSession) {
603
+ this.dispatchEvent(new CustomEvent('sol-login', {
604
+ bubbles: true, composed: true,
605
+ detail: {
606
+ webId: firstSession.info.webId,
607
+ issuer: firstSession.info.issuer
608
+ }
609
+ }));
610
+ }
611
+ }
612
+
613
+ async logout() {
614
+ if (this._mode === 'popup') {
615
+ // Log out only this element's side.
616
+ const session = this._sideSession();
617
+ if (session) {
618
+ try { await session.logout(); } catch (e) {}
619
+ this._auth.sessions.delete(this._side);
620
+ }
621
+ this._popupWindow = null;
622
+ this._updateUI();
623
+ this._integrateWithRdflib();
624
+ this.dispatchEvent(new CustomEvent('sol-logout', {
625
+ bubbles: true, composed: true, detail: { side: this._side },
626
+ }));
627
+ return;
628
+ }
629
+ for (const [, session] of this._auth.sessions) {
630
+ if (session.info?.isLoggedIn) {
631
+ await session.logout();
632
+ }
633
+ }
634
+ this._updateUI();
635
+ this._integrateWithRdflib();
636
+ this.dispatchEvent(new CustomEvent('sol-logout', { bubbles: true, composed: true }));
637
+ }
638
+
639
+ _integrateWithRdflib() {
640
+ const win = typeof window !== 'undefined' ? window : {};
641
+
642
+ // Route rdflib's Fetcher (and anything else we patch) through solFetch
643
+ // so a 401 from UpdateManager-driven saves (sol-form), sol-query SPARQL
644
+ // calls, sol-include document loads, etc. triggers `sol-auth-needed`
645
+ // and gets the chrome's login UX + auto-retry. solFetch internally
646
+ // calls am.fetchFor under the hood, so an already-authenticated
647
+ // request still goes through the right session.
648
+ const authFetchWrapper = (uri, options = {}) => solFetch(uri, options);
649
+
650
+ const patchFetcherCtor = (FetcherCtor) => {
651
+ if (!FetcherCtor?.prototype) return;
652
+ const proto = FetcherCtor.prototype;
653
+ if (!proto._originalFetch) {
654
+ proto._originalFetch = proto.fetch || proto._fetch || fetch;
655
+ }
656
+ if (proto.fetch) proto.fetch = authFetchWrapper;
657
+ if (proto._fetch) proto._fetch = authFetchWrapper;
658
+ };
659
+
660
+ const patchFetcherInstance = (fetcher) => {
661
+ if (!fetcher) return;
662
+ if (!fetcher._originalFetch) {
663
+ fetcher._originalFetch = fetcher.fetch || fetcher._fetch || fetch;
664
+ }
665
+ if (fetcher.fetch) fetcher.fetch = authFetchWrapper;
666
+ if (fetcher._fetch) fetcher._fetch = authFetchWrapper;
667
+ };
668
+
669
+ // 1. Patch Fetcher constructors (host-page global + our singleton) so any
670
+ // future `new Fetcher(...)` call gets auth.
671
+ patchFetcherCtor(win.$rdf?.Fetcher);
672
+ if (rdf.isReady()) patchFetcherCtor(rdf.Fetcher);
673
+
674
+ // 2. Adopt an external shared store if one is already on the page. This
675
+ // makes our components and solid-logic / solid-ui / mashlib share one
676
+ // rdflib graph (same cache, same subscriptions), so data loaded by
677
+ // either side is visible to the other.
678
+ // Probes solid-logic (`window.SolidLogic.store`), solid-ui / mashlib
679
+ // (`window.UI.store`), and the older `window.panes.store` surface.
680
+ const externalStore =
681
+ win.SolidLogic?.store
682
+ || win.UI?.store
683
+ || win.panes?.store
684
+ || null;
685
+ if (externalStore && rdf.isReady()) rdf.useStore(externalStore);
686
+
687
+ // 2b. If nothing was on the page, publish our singleton upward so
688
+ // mashlib/solid-ui/solid-logic loaded *after* us share our graph.
689
+ if (!externalStore && rdf.isReady() && !win.SolidLogic) {
690
+ win.SolidLogic = { store: rdf.store, fetcher: rdf.storeFetcher };
691
+ }
692
+
693
+ // 3. Patch any already-instantiated Fetcher instances hanging off the
694
+ // shared store(s), so existing rdflib code paths also get auth.
695
+ patchFetcherInstance(win.SolidLogic?.store?.fetcher);
696
+ patchFetcherInstance(win.UI?.store?.fetcher);
697
+ patchFetcherInstance(win.panes?.store?.fetcher);
698
+ if (rdf.isReady()) patchFetcherInstance(rdf._fetcher);
699
+ }
700
+
701
+ _render() {
702
+ const s = this.shadowRoot;
703
+ s.innerHTML = `
704
+ <span class="auth-status"></span>
705
+ <button class="sol-btn sol-btn-sm sol-btn-primary auth-btn">Log in</button>
706
+ <div class="dropdown">
707
+ <div class="issuer-list"></div>
708
+ <div class="custom-row">
709
+ <input class="sol-input issuer-input" type="text" placeholder="https://your-issuer.org">
710
+ <button class="sol-btn sol-btn-sm sol-btn-primary">Log in</button>
711
+ </div>
712
+ </div>`;
713
+ s.adoptedStyleSheets = [];
714
+ adopt(s, { sheet: LOGIN_SHEET, css: CSS });
715
+
716
+ const mainBtn = s.querySelector('.auth-btn');
717
+ mainBtn.addEventListener('click', () => this._handleClick());
718
+
719
+ const goBtn = s.querySelector('.custom-row .sol-btn');
720
+ goBtn.addEventListener('click', () => this._loginCustom());
721
+
722
+ const input = s.querySelector('.issuer-input');
723
+ input.addEventListener('keydown', (e) => {
724
+ if (e.key === 'Enter') this._loginCustom();
725
+ });
726
+ }
727
+
728
+ _handleClick() {
729
+ if (this.isLoggedIn) {
730
+ this.logout();
731
+ } else {
732
+ this._toggleDropdown();
733
+ }
734
+ }
735
+
736
+ _toggleDropdown() {
737
+ const dd = this.shadowRoot.querySelector('.dropdown');
738
+ if (dd.classList.contains('open')) {
739
+ this._closeDropdown();
740
+ return;
741
+ }
742
+ this._renderIssuers();
743
+
744
+ const btn = this.shadowRoot.querySelector('.auth-btn');
745
+ const rect = btn.getBoundingClientRect();
746
+ dd.style.top = (rect.bottom + 4) + 'px';
747
+ dd.classList.add('open');
748
+ requestAnimationFrame(() => {
749
+ const dw = dd.offsetWidth;
750
+ const clampedLeft = Math.max(4, Math.min(rect.right - dw, window.innerWidth - dw - 4));
751
+ dd.style.left = clampedLeft + 'px';
752
+ });
753
+
754
+ const input = this.shadowRoot.querySelector('.issuer-input');
755
+ input.value = this._issuers[0] || '';
756
+ input.focus();
757
+
758
+ const close = (e) => {
759
+ if (!dd.contains(e.composedPath()[0]) && e.composedPath()[0] !== btn) {
760
+ this._closeDropdown();
761
+ document.removeEventListener('click', close, true);
762
+ }
763
+ };
764
+ setTimeout(() => document.addEventListener('click', close, true), 0);
765
+ }
766
+
767
+ _closeDropdown() {
768
+ const dd = this.shadowRoot.querySelector('.dropdown');
769
+ if (dd) dd.classList.remove('open');
770
+ }
771
+
772
+ /** Insert (or update) a tiny hint at the top of the dropdown that
773
+ * names the default issuer auto-login is using and prompts the user
774
+ * to pick another to switch. Idempotent — calling twice updates
775
+ * the hint text instead of stacking it. */
776
+ _showSwitchHint(defaultIssuer) {
777
+ const dd = this.shadowRoot.querySelector('.dropdown');
778
+ if (!dd) return;
779
+ let hint = dd.querySelector('.switch-hint');
780
+ if (!hint) {
781
+ hint = document.createElement('div');
782
+ hint.className = 'switch-hint';
783
+ dd.insertBefore(hint, dd.firstChild);
784
+ }
785
+ const short = defaultIssuer.replace(/^https?:\/\//, '').replace(/\/$/, '');
786
+ hint.textContent = `Signing in as ${short} — pick another to switch`;
787
+ }
788
+
789
+ _hideSwitchHint() {
790
+ const hint = this.shadowRoot.querySelector('.switch-hint');
791
+ if (hint) hint.remove();
792
+ }
793
+
794
+ _renderIssuers() {
795
+ const list = this.shadowRoot.querySelector('.issuer-list');
796
+ if (!list) return;
797
+ list.innerHTML = '';
798
+ this._issuers.forEach(issuer => {
799
+ const btn = document.createElement('button');
800
+ btn.className = 'issuer-item';
801
+ btn.textContent = issuer.replace(/^https?:\/\//, '');
802
+ btn.title = issuer;
803
+ btn.onclick = () => {
804
+ this._closeDropdown();
805
+ const url = issuer.endsWith('/') ? issuer : issuer + '/';
806
+ this.login(url);
807
+ };
808
+ list.appendChild(btn);
809
+ });
810
+ }
811
+
812
+ async _loginCustom() {
813
+ const input = this.shadowRoot.querySelector('.issuer-input');
814
+ const val = input.value.trim();
815
+ if (!val) return;
816
+ const issuer = val.endsWith('/') ? val : val + '/';
817
+ this._closeDropdown();
818
+ await this.login(issuer);
819
+ }
820
+
821
+ /** Show a transient message in the status span (overwritten by _updateUI). */
822
+ _setStatusMessage(msg, isErr) {
823
+ const status = this.shadowRoot && this.shadowRoot.querySelector('.auth-status');
824
+ if (!status) return;
825
+ status.textContent = msg;
826
+ status.className = 'auth-status' + (isErr ? ' auth-error' : '');
827
+ }
828
+
829
+ _updateUI() {
830
+ const status = this.shadowRoot.querySelector('.auth-status');
831
+ const btn = this.shadowRoot.querySelector('.auth-btn');
832
+ if (!status || !btn) return;
833
+
834
+ const session = this._mode === 'popup'
835
+ ? this._sideSession()
836
+ : this._auth.getFirstLoggedIn();
837
+ // The WebID is surfaced only as the button's hover title, never as
838
+ // visible page text.
839
+ status.textContent = '';
840
+ if (session && session.info && session.info.isLoggedIn) {
841
+ status.className = 'auth-status logged-in';
842
+ btn.textContent = 'Log out';
843
+ btn.className = 'sol-btn sol-btn-sm auth-btn logged-in';
844
+ btn.title = session.info.webId || '';
845
+ } else {
846
+ status.className = 'auth-status';
847
+ btn.textContent = 'Log in';
848
+ btn.className = 'sol-btn sol-btn-sm sol-btn-primary auth-btn';
849
+ btn.title = '';
850
+ }
851
+ }
852
+ }
853
+
854
+ define('sol-login', SolLogin);
855
+ export { SolLogin, AuthManager };
856
+ export default SolLogin;