fedipod 1.45.0 → 1.48.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 (54) hide show
  1. package/README.md +11 -16
  2. package/architecture.md +6 -3
  3. package/gateway.md +51 -23
  4. package/gui.md +3 -2
  5. package/lib/client/c2s.mjs +2 -1
  6. package/lib/client/masto/index.mjs +5 -4
  7. package/lib/client/masto/render.mjs +4 -1
  8. package/lib/core/intake/activities.mjs +8 -9
  9. package/lib/core/intake/counts.mjs +143 -0
  10. package/lib/core/intake/index.mjs +4 -0
  11. package/lib/core/publisher/notes.mjs +4 -1
  12. package/lib/core/social.mjs +2 -0
  13. package/lib/core/storage.mjs +54 -5
  14. package/lib/core/wire.mjs +16 -5
  15. package/lib/gateway/account-agent.mjs +20 -4
  16. package/lib/gateway/copy.mjs +95 -5
  17. package/lib/gateway/front-core.mjs +4 -4
  18. package/lib/gateway/held-mail.mjs +11 -4
  19. package/lib/gateway/keeper.mjs +9 -6
  20. package/lib/gateway/masto-gateway.mjs +140 -40
  21. package/lib/gateway/private-read.mjs +10 -3
  22. package/lib/gateway/quiet.mjs +4 -3
  23. package/lib/gateway/state-api.mjs +73 -18
  24. package/lib/pod/notes.mjs +25 -0
  25. package/lib/server/embed.mjs +1 -1
  26. package/lib/session/README.md +2 -135
  27. package/lib/surface/routes/owner.mjs +2 -2
  28. package/lib/surface/surface.mjs +4 -3
  29. package/package.json +5 -3
  30. package/scripts/check-netlify-bundles.mjs +0 -4
  31. package/scripts/check-pod-calls.mjs +5 -108
  32. package/scripts/check-sizes.mjs +6 -3
  33. package/scripts/dependents.mjs +76 -0
  34. package/scripts/stage-site.mjs +10 -8
  35. package/scripts/start.mjs +2 -2
  36. package/scripts/test-dependents.mjs +37 -0
  37. package/web/admin/gateway.js +8 -24
  38. package/web/admin/index.html +0 -16
  39. package/web/admin/record.js +1 -2
  40. package/web/admin/upkeep.js +1 -5
  41. package/web/app/admin-facade.mjs +1 -1
  42. package/web/app/agent.mjs +27 -10
  43. package/web/app/copy-mode.mjs +98 -13
  44. package/web/app/dist/boot.js +68 -57
  45. package/web/app/dist/boot.js.map +4 -4
  46. package/web/app/dist/sw.js +583 -283
  47. package/web/app/dist/sw.js.map +4 -4
  48. package/web/app/forum-support.mjs +2 -2
  49. package/web/app/oidc-session.mjs +2 -2
  50. package/lib/session/demo.html +0 -94
  51. package/lib/session/fedi-account.mjs +0 -417
  52. package/lib/session/fedi-login.mjs +0 -123
  53. package/lib/session/oidc-session.mjs +0 -233
  54. package/lib/session/package.json +0 -49
@@ -1,123 +0,0 @@
1
- // fedi-login.mjs — sign a person in at their own Solid pod from their
2
- // Fediverse address, and bring them back to where they were.
3
- //
4
- // import { fediLogin } from './fedi-login.mjs';
5
- // const login = fediLogin({ dbName: 'my-app-oidc', clientName: 'My app' });
6
- //
7
- // await login.login('@mei@fedipod.net'); // leaves for the pod's login page
8
- // await login.resume(); // on every page load: finishes a
9
- // // login in progress and returns them
10
- // const s = await login.getSession(); // null, or { webId, issuer, fetch, signOut }
11
- //
12
- // How an address becomes a login page. WebFinger at the address's host names
13
- // the actor and its aliases. An origin among those that answers
14
- // /.well-known/openid-configuration is the login provider, or says which is
15
- // (a pod host redirects there). Failing that, the WebID — the URL typed, or
16
- // the ones the actor names as its own — is read as JSON-LD and its
17
- // solid:oidcIssuer is the provider. A Mastodon address has neither, and the
18
- // person is told so in words.
19
- //
20
- // No imports but the session engine beside it, no Node built-ins, nothing
21
- // named after the application that uses it.
22
- import { solidOidcSession } from './oidc-session.mjs';
23
-
24
- const OIDC_ISSUER = 'http://www.w3.org/ns/solid/terms#oidcIssuer';
25
-
26
- /** `@mei@host`, `mei@host`, or a WebID or actor URL. Null when it is neither. */
27
- export function parseAddress(input) {
28
- const t = String(input || '').trim();
29
- if (/^https?:\/\//u.test(t)) { try { return { url: new URL(t).href, host: new URL(t).host }; } catch { return null; } }
30
- const m = /^@?([^@\s/]+)@([^@\s/]+)$/u.exec(t);
31
- if (!m) return null;
32
- const name = m[1]; const host = m[2].toLowerCase();
33
- return { name, host, at: `@${name}@${host}` };
34
- }
35
-
36
- export function fediLogin({ dbName, clientName, redirectUri = null, fetch: f = globalThis.fetch?.bind(globalThis) } = {}) {
37
- const oidc = solidOidcSession({ dbName, clientName });
38
- const get = (url, accept) => f(url, { headers: { accept }, redirect: 'follow' });
39
- const json = (url, accept) => get(url, accept).then((r) => (r.ok ? r.json() : null)).catch(() => null);
40
- const here = () => (globalThis.location ? location.origin + location.pathname : null);
41
-
42
- const discovery = (base) => json(`${String(base).replace(/\/+$/, '')}/.well-known/openid-configuration`, 'application/json');
43
- const isProvider = (cfg) => !!(cfg?.issuer && cfg.authorization_endpoint);
44
- const origin = (u) => { try { return new URL(u).origin; } catch { return null; } };
45
-
46
- // The issuer a WebID document names, read as the expanded JSON-LD a pod answers.
47
- const issuerOfWebId = async (webId) => {
48
- const doc = await json(webId, 'application/ld+json');
49
- const nodes = Array.isArray(doc) ? doc : (doc?.['@graph'] || (doc ? [doc] : []));
50
- const me = nodes.find((n) => n?.['@id'] === webId && n[OIDC_ISSUER]) || nodes.find((n) => n?.[OIDC_ISSUER]);
51
- const v = [].concat(me?.[OIDC_ISSUER] || [])[0];
52
- return typeof v === 'string' ? v : (v?.['@id'] || null);
53
- };
54
-
55
- /** Where an address signs in: { address, actor, origin, issuer, webId? }.
56
- * Throws, in plain words, when the host does not know the address or
57
- * nothing about it names a login provider. */
58
- async function resolve(address) {
59
- const a = parseAddress(address);
60
- if (!a) throw new Error('an address looks like @you@your.server');
61
- let actor = null;
62
- const urls = [];
63
- if (a.url) urls.push(a.url);
64
- else {
65
- const r = await get(`https://${a.host}/.well-known/webfinger?resource=${encodeURIComponent(`acct:${a.name}@${a.host}`)}`,
66
- 'application/jrd+json, application/json').catch(() => null);
67
- if (!r?.ok) throw new Error(`${a.host} does not know ${a.at}`);
68
- const jrd = await r.json().catch(() => null);
69
- const self = (jrd?.links || []).find((l) => l.rel === 'self' && /activity\+json|ld\+json/u.test(l.type || ''));
70
- actor = self?.href || null;
71
- // The aliases first: at a Gateway they name the actor on the pod itself,
72
- // and the pod is where the login is.
73
- for (const u of [...(jrd?.aliases || []), ...(actor ? [actor] : [])]) if (typeof u === 'string') urls.push(u);
74
- if (!urls.length) throw new Error(`${a.host} did not say where ${a.at} lives`);
75
- }
76
- const found = (extra) => ({ address: a.at || a.url, actor, ...extra });
77
-
78
- // 1. An origin that is the login provider, or says which is.
79
- for (const o of [...new Set(urls.map(origin).filter(Boolean))]) {
80
- const cfg = await discovery(o);
81
- if (isProvider(cfg)) return found({ origin: o, issuer: cfg.issuer });
82
- }
83
- // 2. The WebID: the URL typed, or the ones the actor names as its own.
84
- const webIds = a.url ? [a.url] : [];
85
- if (actor) {
86
- const doc = await json(actor, 'application/activity+json');
87
- for (const u of [].concat(doc?.alsoKnownAs || [])) if (typeof u === 'string') webIds.push(u);
88
- }
89
- for (const webId of webIds) {
90
- const issuer = await issuerOfWebId(webId);
91
- if (issuer && isProvider(await discovery(issuer))) return found({ origin: origin(webId), issuer, webId });
92
- }
93
- throw new Error(`${a.host} is not a Solid pod, so there is no pod login to send you to`);
94
- }
95
-
96
- /** Everything `login` does short of leaving the page: returns the found pod
97
- * and the `authorizationUrl` to send the person to. */
98
- async function startLogin(address, { returnTo = globalThis.location?.href || null, redirectUri: ru = redirectUri || here() } = {}) {
99
- const found = await resolve(address);
100
- const { authorizationUrl } = await oidc.beginLogin({ issuer: found.issuer, redirectUri: ru, returnTo });
101
- return { ...found, authorizationUrl, returnTo };
102
- }
103
-
104
- /** Send the person to their pod's login page. They come back to `redirectUri`
105
- * (this page, unless set) and `resume` there returns them to `returnTo`
106
- * (where they were, unless set). */
107
- async function login(address, opts) {
108
- const s = await startLogin(address, opts);
109
- location.href = s.authorizationUrl;
110
- return s;
111
- }
112
-
113
- /** Call on every page load. Finishes a login the person is coming back from
114
- * and returns them to where they were; null when no login is in progress. */
115
- async function resume({ currentUrl = globalThis.location?.href, go = (u) => location.replace(u) } = {}) {
116
- const session = await oidc.completeLogin({ currentUrl });
117
- if (!session) return null;
118
- if (session.returnTo && session.returnTo !== currentUrl) go(session.returnTo);
119
- return session;
120
- }
121
-
122
- return { resolve, startLogin, login, resume, getSession: oidc.getSession, signOut: oidc.signOut };
123
- }
@@ -1,233 +0,0 @@
1
- // oidc-session.mjs — Solid-OIDC login for a browser page, with a secure,
2
- // durable, worker-visible session.
3
- //
4
- // The redirect handshake (authorization-code + PKCE) runs in the page. The
5
- // session it yields — a DPoP keypair whose private half is NON-EXTRACTABLE, plus
6
- // the refresh token, the token endpoint and the WebID — is kept in IndexedDB.
7
- // IndexedDB survives a browser restart and is readable by a service worker,
8
- // and a non-extractable key can sign but never be read out, even by injected
9
- // script.
10
- //
11
- // import { solidOidcSession } from 'solid-oidc-session/oidc-session.mjs';
12
- // const oidc = solidOidcSession({ dbName: 'my-app-oidc', clientName: 'My app' });
13
- //
14
- // Page: const { authorizationUrl } = await oidc.beginLogin({ issuer, redirectUri });
15
- // location.href = authorizationUrl; // ... redirect ...
16
- // await oidc.completeLogin({ currentUrl: location.href }); // on return
17
- // Anywhere (page or worker, SAME dbName):
18
- // const session = await oidc.getSession(); // { webId, issuer, fetch, refresh, signOut } | null
19
- //
20
- // No imports, no Node built-ins, nothing named after the application that
21
- // uses it: IndexedDB, WebCrypto and fetch are all it needs.
22
-
23
- const STORE = 'session';
24
- const b64u = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
25
- const enc = (o) => b64u(new TextEncoder().encode(JSON.stringify(o)));
26
- const sha256 = (s) => crypto.subtle.digest('SHA-256', new TextEncoder().encode(s));
27
- const rand = (n = 32) => b64u(crypto.getRandomValues(new Uint8Array(n)));
28
-
29
- async function dpopKey() { return crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']); }
30
- async function publicJwk(pair) { const j = await crypto.subtle.exportKey('jwk', pair.publicKey); return { kty: j.kty, crv: j.crv, x: j.x, y: j.y }; }
31
- async function dpopProof(pair, jwk, htm, htu, ath) {
32
- const header = { typ: 'dpop+jwt', alg: 'ES256', jwk };
33
- const payload = { htm, htu: htu.split('#')[0], jti: crypto.randomUUID(), iat: Math.floor(Date.now() / 1000), ...(ath ? { ath } : {}) };
34
- const data = `${enc(header)}.${enc(payload)}`;
35
- const sig = await crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, pair.privateKey, new TextEncoder().encode(data));
36
- return `${data}.${b64u(sig)}`;
37
- }
38
- const jwtPayload = (jwt) => { try { return JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(jwt.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0)))); } catch { return {}; } };
39
-
40
- async function discover(issuer) {
41
- const res = await fetch(`${issuer.replace(/\/+$/, '')}/.well-known/openid-configuration`, { headers: { accept: 'application/json' } });
42
- if (!res.ok) throw new Error(`OIDC discovery failed at ${issuer} (HTTP ${res.status})`);
43
- return res.json();
44
- }
45
- async function registerClient(cfg, redirectUri, clientName) {
46
- const res = await fetch(cfg.registration_endpoint, {
47
- method: 'POST', headers: { 'content-type': 'application/json' },
48
- body: JSON.stringify({ client_name: clientName, redirect_uris: [redirectUri], token_endpoint_auth_method: 'none',
49
- grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], application_type: 'web', scope: 'openid webid offline_access' }),
50
- });
51
- if (!res.ok) throw new Error(`client registration failed (HTTP ${res.status})`);
52
- return (await res.json()).client_id;
53
- }
54
-
55
- /**
56
- * One session store. `dbName` is the IndexedDB database the session lives in:
57
- * a page and its service worker must pass the same name to see the same
58
- * sign-in, and two apps on one origin pass different names to keep theirs
59
- * apart. `clientName` is what the pod's login screen shows the person.
60
- */
61
- export function solidOidcSession({ dbName = 'solid-oidc-session', clientName = 'Solid app' } = {}) {
62
- function idb() {
63
- return new Promise((res, rej) => {
64
- const r = indexedDB.open(dbName, 1);
65
- r.onupgradeneeded = () => r.result.createObjectStore(STORE);
66
- r.onsuccess = () => res(r.result);
67
- r.onerror = () => rej(r.error);
68
- });
69
- }
70
- async function idbGet(key) {
71
- const db = await idb();
72
- return new Promise((res, rej) => { const t = db.transaction(STORE).objectStore(STORE).get(key); t.onsuccess = () => res(t.result); t.onerror = () => rej(t.error); });
73
- }
74
- async function idbPut(key, val) {
75
- const db = await idb();
76
- return new Promise((res, rej) => { const t = db.transaction(STORE, 'readwrite').objectStore(STORE).put(val, key); t.onsuccess = () => res(); t.onerror = () => rej(t.error); });
77
- }
78
- async function idbDel(key) {
79
- const db = await idb();
80
- return new Promise((res, rej) => { const t = db.transaction(STORE, 'readwrite').objectStore(STORE).delete(key); t.onsuccess = () => res(); t.onerror = () => rej(t.error); });
81
- }
82
-
83
- /** Begin login: prepare PKCE + a non-extractable DPoP key, stash the pending
84
- * state, and return the authorization URL for the caller to navigate to. */
85
- async function beginLogin({ issuer, redirectUri, clientName: name = clientName, returnTo = null }) {
86
- const cfg = await discover(issuer);
87
- const client_id = await registerClient(cfg, redirectUri, name);
88
- const pair = await dpopKey();
89
- const verifier = rand(48);
90
- const challenge = b64u(await sha256(verifier));
91
- const state = rand(16);
92
- await idbPut('pending', { issuer, client_id, redirectUri, verifier, state, pair, returnTo,
93
- tokenEndpoint: cfg.token_endpoint, authorizationEndpoint: cfg.authorization_endpoint,
94
- revocationEndpoint: cfg.revocation_endpoint || null });
95
- const url = new URL(cfg.authorization_endpoint);
96
- for (const [k, v] of Object.entries({ client_id, redirect_uri: redirectUri, response_type: 'code',
97
- scope: 'openid webid offline_access', code_challenge: challenge, code_challenge_method: 'S256', state, prompt: 'consent' })) url.searchParams.set(k, v);
98
- return { authorizationUrl: url.href };
99
- }
100
-
101
- /** Complete login from the redirect back. Exchanges the code (DPoP + PKCE),
102
- * stores the durable session, and returns it. Returns null if there is no code. */
103
- async function completeLogin({ currentUrl }) {
104
- const u = new URL(currentUrl);
105
- const code = u.searchParams.get('code');
106
- const state = u.searchParams.get('state');
107
- if (!code) return null;
108
- const p = await idbGet('pending');
109
- if (!p || p.state !== state) throw new Error('login state mismatch');
110
- const jwk = await publicJwk(p.pair);
111
- const res = await fetch(p.tokenEndpoint, {
112
- method: 'POST',
113
- headers: { 'content-type': 'application/x-www-form-urlencoded', dpop: await dpopProof(p.pair, jwk, 'POST', p.tokenEndpoint) },
114
- body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: p.redirectUri, client_id: p.client_id, code_verifier: p.verifier }),
115
- });
116
- const tok = await res.json().catch(() => ({}));
117
- if (!res.ok || !tok.access_token) throw new Error(`token exchange failed (HTTP ${res.status}): ${tok.error || ''}`);
118
- const webId = jwtPayload(tok.access_token).webid || jwtPayload(tok.id_token || '').webid || null;
119
- const session = { issuer: p.issuer, client_id: p.client_id, tokenEndpoint: p.tokenEndpoint, pair: p.pair,
120
- // Where to hand the refresh token back at sign-out. Kept on the session
121
- // because discovery is a network round trip and sign-out should not need
122
- // one — see signOut().
123
- revocationEndpoint: p.revocationEndpoint || null,
124
- refreshToken: tok.refresh_token || null, accessToken: tok.access_token,
125
- expiresAt: Date.now() + Math.max(30, (tok.expires_in || 300)) * 1000, webId };
126
- await idbPut('session', session);
127
- await idbDel('pending');
128
- // `returnTo` rides only on the handle this call returns: where the person
129
- // was when they left for the login page, for the caller to send them back.
130
- return Object.assign(sessionHandle(session), p.returnTo ? { returnTo: p.returnTo } : {});
131
- }
132
-
133
- /** Restore the session from IndexedDB — works in the page and the worker. */
134
- async function getSession() {
135
- const s = await idbGet('session');
136
- return s ? sessionHandle(s) : null;
137
- }
138
-
139
- /**
140
- * Sign out, and tell the issuer.
141
- *
142
- * Dropping the row locally leaves the REFRESH TOKEN alive at the pod for its
143
- * full life — signing out of a shared or borrowed browser would clear the
144
- * screen and nothing else, and anyone who had copied the token could go on
145
- * minting access tokens with it. RFC 7009: hand it back.
146
- *
147
- * Best effort, and the local teardown happens either way: an issuer that is
148
- * unreachable, or that publishes no revocation endpoint, must not be able to
149
- * keep somebody signed in.
150
- */
151
- async function signOut() {
152
- const s = await idbGet('session').catch(() => null);
153
- if (s?.revocationEndpoint && s.refreshToken) {
154
- try {
155
- await fetch(s.revocationEndpoint, {
156
- method: 'POST',
157
- headers: { 'content-type': 'application/x-www-form-urlencoded' },
158
- body: new URLSearchParams({
159
- token: s.refreshToken, token_type_hint: 'refresh_token', client_id: s.client_id,
160
- }),
161
- });
162
- } catch { /* told if we could; leaving is not conditional on being heard */ }
163
- }
164
- await idbDel('session');
165
- await idbDel('pending');
166
- }
167
-
168
- function sessionHandle(s) {
169
- let { accessToken, expiresAt, refreshToken } = s;
170
- const jwkP = publicJwk(s.pair);
171
- const refresh = async () => {
172
- if (!refreshToken) throw new Error('session expired and there is no refresh token — sign in again');
173
- const jwk = await jwkP;
174
- let res;
175
- try {
176
- res = await fetch(s.tokenEndpoint, {
177
- method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded', dpop: await dpopProof(s.pair, jwk, 'POST', s.tokenEndpoint) },
178
- body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: s.client_id, scope: 'openid webid offline_access' }),
179
- });
180
- } catch (e) {
181
- // A renewal that never reached the server is not a refused one: the
182
- // session is still good, and saying "your pod refused the write" about
183
- // it sends whoever reads it looking in the wrong place. Under load a
184
- // server refuses the token endpoint first, and a refusal with no CORS
185
- // headers arrives here as the browser's bare "Failed to fetch".
186
- throw new Error(`${e.message} — renewing your sign-in at ${new URL(s.tokenEndpoint).host}`);
187
- }
188
- const tok = await res.json().catch(() => ({}));
189
- if (res.status === 429) {
190
- throw new Error(`${new URL(s.tokenEndpoint).host} is asking us to slow down — your sign-in could not be renewed just now`);
191
- }
192
- if (!res.ok || !tok.access_token) { await idbDel('session'); throw new Error('refresh failed — sign in again'); }
193
- accessToken = tok.access_token;
194
- expiresAt = Date.now() + Math.max(30, (tok.expires_in || 300)) * 1000;
195
- if (tok.refresh_token) refreshToken = tok.refresh_token;
196
- // Re-read before writing back. There is ONE 'session' row, and this handle
197
- // closed over the session it was made from — so a refresh that was already
198
- // in flight when the user signed in as somebody else would put the OLD
199
- // identity back, silently, over the new one. Only write if the row is still
200
- // the session this handle belongs to.
201
- const now = await idbGet('session');
202
- if (now && now.webId !== s.webId) {
203
- throw new Error('signed in as somebody else while this session was refreshing');
204
- }
205
- await idbPut('session', { ...s, accessToken, expiresAt, refreshToken });
206
- };
207
- const send = async (url, init) => {
208
- const jwk = await jwkP;
209
- const ath = b64u(await sha256(accessToken));
210
- const headers = { ...(init.headers || {}), authorization: `DPoP ${accessToken}`, dpop: await dpopProof(s.pair, jwk, (init.method || 'GET'), url, ath) };
211
- return fetch(url, { ...init, headers });
212
- };
213
- const authFetch = async (url, init = {}) => {
214
- if (Date.now() > expiresAt - 30_000) await refresh();
215
- const res = await send(url, init);
216
- // A token the pod REFUSES is not only ever an expired one: a pod that
217
- // restarted, a clock that drifted, a token the server stopped recognising
218
- // all answer 401 while the clock here still says the token is good — so the
219
- // renewal above never fires and every call fails from then on. Renew once
220
- // and ask again.
221
- if (res.status === 401 && refreshToken) {
222
- try { await refresh(); } catch { return res; }
223
- return send(url, init);
224
- }
225
- return res;
226
- };
227
- // `issuer` so a page that cannot renew can send the person back to the right
228
- // login without asking them for their address again.
229
- return { webId: s.webId, issuer: s.issuer, fetch: authFetch, refresh, signOut };
230
- }
231
-
232
- return { beginLogin, completeLogin, getSession, signOut };
233
- }
@@ -1,49 +0,0 @@
1
- {
2
- "name": "fediverse-session",
3
- "version": "0.2.1",
4
- "description": "Type a Fediverse handle or a WebID and get back an account you can act with: post, read its timeline, follow, reply, favourite, boost. Works out whether the account lives on a Mastodon-family server or on a Solid pod through FediPod, sends the person to sign in there, brings them back to where they were, and speaks to whichever it is.",
5
- "type": "module",
6
- "license": "MIT",
7
- "author": "Jeff Zucker",
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/jeff-zucker/FediPod.git",
11
- "directory": "lib/session"
12
- },
13
- "homepage": "https://github.com/jeff-zucker/FediPod/tree/main/lib/session#readme",
14
- "keywords": [
15
- "fediverse",
16
- "activitypub",
17
- "mastodon",
18
- "solid",
19
- "solid-oidc",
20
- "webid",
21
- "login",
22
- "oauth",
23
- "fedipod"
24
- ],
25
- "files": [
26
- "fedi-account.mjs",
27
- "fedi-login.mjs",
28
- "oidc-session.mjs",
29
- "README.md"
30
- ],
31
- "exports": {
32
- ".": "./fedi-account.mjs",
33
- "./fedi-account.mjs": "./fedi-account.mjs",
34
- "./fedi-login.mjs": "./fedi-login.mjs",
35
- "./oidc-session.mjs": "./oidc-session.mjs"
36
- },
37
- "scripts": {
38
- "prepublishOnly": "node ../../scripts/check-pod-calls.mjs"
39
- },
40
- "dependencies": {},
41
- "peerDependencies": {
42
- "jsonld": "^9.0.0"
43
- },
44
- "peerDependenciesMeta": {
45
- "jsonld": {
46
- "optional": true
47
- }
48
- }
49
- }