javascript-solid-server 0.0.97 → 0.0.98

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.97",
3
+ "version": "0.0.98",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/ap/index.js CHANGED
@@ -148,7 +148,7 @@ export async function activityPubPlugin(fastify, options = {}) {
148
148
  version: '2.1',
149
149
  software: {
150
150
  name: 'jss',
151
- version: '0.0.97',
151
+ version: '0.0.98',
152
152
  repository: 'https://github.com/JavaScriptSolidServer/JavaScriptSolidServer'
153
153
  },
154
154
  protocols: ['activitypub', 'solid'],
@@ -115,7 +115,7 @@ export function createInstanceHandler (config) {
115
115
  title: config.displayName || 'JSS',
116
116
  description: 'SAND Stack: Solid + ActivityPub + Nostr + DID',
117
117
  short_description: 'Solid pod with Mastodon-compatible API',
118
- version: '4.0.0 (compatible; JSS 0.0.97)',
118
+ version: '4.0.0 (compatible; JSS 0.0.98)',
119
119
  urls: {
120
120
  streaming_api: `${wsProtocol}://${host}`
121
121
  },
@@ -46,12 +46,19 @@ function parseBody (request) {
46
46
  * Validate client_id and redirect_uri against registered client
47
47
  * Returns { client, error } — client is null if validation fails
48
48
  */
49
- function validateClient (clientId, redirectUri) {
49
+ function validateClient (clientId, redirectUri, responseType) {
50
50
  if (!clientId || !redirectUri) {
51
51
  return { client: null, error: 'Missing client_id or redirect_uri' }
52
52
  }
53
53
 
54
54
  const client = getClient(clientId)
55
+
56
+ // Implicit flow (response_type=token) allows unregistered clients
57
+ // remoteStorage clients pass their origin URL as client_id without pre-registration
58
+ if (!client && responseType === 'token') {
59
+ return { client: { name: clientId, redirect_uri: redirectUri }, error: null }
60
+ }
61
+
55
62
  if (!client) {
56
63
  return { client: null, error: 'Unknown client_id. Register via POST /api/v1/apps first.' }
57
64
  }
@@ -71,17 +78,17 @@ export function createAuthorizeHandler () {
71
78
  return async (request, reply) => {
72
79
  const { client_id, redirect_uri, response_type, scope, state } = request.query
73
80
 
74
- if (response_type && response_type !== 'code') {
75
- return reply.code(400).send({ error: 'unsupported_response_type', error_description: 'Only response_type=code is supported' })
81
+ if (response_type && response_type !== 'code' && response_type !== 'token') {
82
+ return reply.code(400).send({ error: 'unsupported_response_type', error_description: 'Supported: code, token' })
76
83
  }
77
84
 
78
- const { client, error } = validateClient(client_id, redirect_uri)
85
+ const { client, error } = validateClient(client_id, redirect_uri, response_type)
79
86
  if (!client) {
80
87
  return reply.code(400).send({ error: 'invalid_client', error_description: error })
81
88
  }
82
89
 
83
90
  return reply.type('text/html').send(
84
- loginPage({ clientId: client_id, redirectUri: redirect_uri, scope: scope || 'read', state, clientName: client.name })
91
+ loginPage({ clientId: client_id, redirectUri: redirect_uri, responseType: response_type || 'code', scope: scope || 'read', state, clientName: client.name })
85
92
  )
86
93
  }
87
94
  }
@@ -92,28 +99,47 @@ export function createAuthorizeHandler () {
92
99
  export function createAuthorizePostHandler () {
93
100
  return async (request, reply) => {
94
101
  const body = parseBody(request)
95
- const { username, password, client_id, redirect_uri, scope, state } = body
102
+ const { username, password, client_id, redirect_uri, response_type, scope, state } = body
96
103
 
97
104
  // Validate client + redirect_uri (prevent open redirect via form tampering)
98
- const { client, error: clientError } = validateClient(client_id, redirect_uri)
105
+ const { client, error: clientError } = validateClient(client_id, redirect_uri, response_type)
99
106
  if (!client) {
100
107
  return reply.code(400).send({ error: 'invalid_client', error_description: clientError })
101
108
  }
102
109
 
103
110
  if (!username || !password) {
104
111
  return reply.type('text/html').send(
105
- loginPage({ clientId: client_id, redirectUri: redirect_uri, scope, state, clientName: client.name, error: 'Username and password are required' })
112
+ loginPage({ clientId: client_id, redirectUri: redirect_uri, responseType: response_type || 'code', scope, state, clientName: client.name, error: 'Username and password are required' })
106
113
  )
107
114
  }
108
115
 
109
116
  const account = await authenticate(username, password)
110
117
  if (!account) {
111
118
  return reply.type('text/html').send(
112
- loginPage({ clientId: client_id, redirectUri: redirect_uri, scope, state, clientName: client.name, error: 'Invalid username or password' })
119
+ loginPage({ clientId: client_id, redirectUri: redirect_uri, responseType: response_type || 'code', scope, state, clientName: client.name, error: 'Invalid username or password' })
113
120
  )
114
121
  }
115
122
 
116
- // Generate one-time auth code (10 min TTL)
123
+ // Implicit grant (response_type=token) return token directly in fragment (RFC 6749 §4.2.2)
124
+ // Used by remoteStorage clients
125
+ if (response_type === 'token') {
126
+ const accessToken = createToken(account.webId)
127
+
128
+ // Handle OOB — display token
129
+ if (redirect_uri === OOB_REDIRECT) {
130
+ return reply.type('text/html').send(oobPage(accessToken))
131
+ }
132
+
133
+ // Fragment-based redirect (token MUST be in fragment, not query — RFC 6749 §4.2.2)
134
+ const params = new URLSearchParams()
135
+ params.set('access_token', accessToken)
136
+ params.set('token_type', 'bearer')
137
+ params.set('scope', scope || 'read')
138
+ if (state) params.set('state', state)
139
+ return reply.redirect(`${redirect_uri}#${params.toString()}`)
140
+ }
141
+
142
+ // Authorization code grant (response_type=code) — generate one-time auth code (10 min TTL)
117
143
  const code = crypto.randomUUID()
118
144
  authCodes.set(code, {
119
145
  clientId: client_id,
@@ -196,7 +222,7 @@ export function createTokenHandler () {
196
222
  /**
197
223
  * Minimal login page HTML
198
224
  */
199
- function loginPage ({ clientId, redirectUri, scope, state, clientName, error }) {
225
+ function loginPage ({ clientId, redirectUri, responseType, scope, state, clientName, error }) {
200
226
  const escapedError = error ? escapeHtml(error) : ''
201
227
  const escapedName = escapeHtml(clientName || clientId || 'Unknown app')
202
228
 
@@ -230,6 +256,7 @@ function loginPage ({ clientId, redirectUri, scope, state, clientName, error })
230
256
  <form method="POST" action="/oauth/authorize">
231
257
  <input type="hidden" name="client_id" value="${escapeHtml(clientId || '')}">
232
258
  <input type="hidden" name="redirect_uri" value="${escapeHtml(redirectUri || '')}">
259
+ <input type="hidden" name="response_type" value="${escapeHtml(responseType || 'code')}">
233
260
  <input type="hidden" name="scope" value="${escapeHtml(scope || 'read')}">
234
261
  ${state ? `<input type="hidden" name="state" value="${escapeHtml(state)}">` : ''}
235
262
  <label for="username">Username</label>
@@ -31,7 +31,8 @@ export async function remoteStoragePlugin (fastify, options = {}) {
31
31
  */
32
32
  function getStoragePath (request) {
33
33
  const wildcard = request.params['*'] || ''
34
- return '/' + wildcard
34
+ // Normalize double slashes (RS library appends path to href which ends with /)
35
+ return ('/' + wildcard).replace(/\/\/+/g, '/')
35
36
  }
36
37
 
37
38
  /**