javascript-solid-server 0.0.143 → 0.0.145

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/bin/jss.js CHANGED
@@ -13,6 +13,7 @@ import { Command } from 'commander';
13
13
  import { createServer } from '../src/server.js';
14
14
  import { loadConfig, saveConfig, printConfig, defaults } from '../src/config.js';
15
15
  import { createInvite, listInvites, revokeInvite } from '../src/idp/invites.js';
16
+ import { findByUsername, updatePassword, deleteAccount } from '../src/idp/accounts.js';
16
17
  import { setQuotaLimit, getQuotaInfo, reconcileQuota, formatBytes } from '../src/storage/quota.js';
17
18
  import { parseSize } from '../src/config.js';
18
19
  import crypto from 'crypto';
@@ -637,32 +638,12 @@ program
637
638
  process.env.DATA_ROOT = path.resolve(options.root);
638
639
  }
639
640
 
640
- // Load account by username
641
- const dataRoot = process.env.DATA_ROOT || './data';
642
- const accountsDir = path.join(dataRoot, '.idp', 'accounts');
643
- const indexPath = path.join(accountsDir, '_username_index.json');
644
-
645
- let usernameIndex;
646
- try {
647
- usernameIndex = await fs.readJson(indexPath);
648
- } catch (err) {
649
- if (err.code === 'ENOENT') {
650
- console.error(`Error: No accounts found (missing ${indexPath})`);
651
- process.exit(1);
652
- }
653
- throw err;
654
- }
655
-
656
- const normalizedUsername = username.toLowerCase().trim();
657
- const accountId = usernameIndex[normalizedUsername];
658
- if (!accountId) {
641
+ const account = await findByUsername(username);
642
+ if (!account) {
659
643
  console.error(`Error: User not found: ${username}`);
660
644
  process.exit(1);
661
645
  }
662
646
 
663
- const accountPath = path.join(accountsDir, `${accountId}.json`);
664
- const account = await fs.readJson(accountPath);
665
-
666
647
  // Determine new password
667
648
  let newPassword;
668
649
 
@@ -673,8 +654,8 @@ program
673
654
  } else {
674
655
  // Interactive prompt
675
656
  newPassword = await promptPassword('New password: ');
676
- const confirm = await promptPassword('Confirm password: ');
677
- if (newPassword !== confirm) {
657
+ const confirmation = await promptPassword('Confirm password: ');
658
+ if (newPassword !== confirmation) {
678
659
  console.error('Error: Passwords do not match');
679
660
  process.exit(1);
680
661
  }
@@ -685,17 +666,63 @@ program
685
666
  process.exit(1);
686
667
  }
687
668
 
688
- // Hash and save
689
- const bcrypt = await import('bcryptjs').then(m => m.default);
690
- account.passwordHash = await bcrypt.hash(newPassword, 10);
691
- account.passwordChangedAt = new Date().toISOString();
692
- await fs.writeJson(accountPath, account, { spaces: 2 });
669
+ await updatePassword(account.id, newPassword);
693
670
 
694
671
  if (options.generate) {
695
- console.log(`\nPassword updated for ${normalizedUsername}`);
672
+ console.log(`\nPassword updated for ${account.username}`);
696
673
  console.log(`Generated password: ${newPassword}\n`);
697
674
  } else {
698
- console.log(`\nPassword updated for ${normalizedUsername}\n`);
675
+ console.log(`\nPassword updated for ${account.username}\n`);
676
+ }
677
+ } catch (err) {
678
+ console.error(`Error: ${err.message}`);
679
+ process.exit(1);
680
+ }
681
+ });
682
+
683
+ /**
684
+ * Account commands - manage user accounts
685
+ */
686
+ const accountCmd = program
687
+ .command('account')
688
+ .description('Manage user accounts');
689
+
690
+ accountCmd
691
+ .command('delete <username>')
692
+ .description('Delete a user account from the IdP')
693
+ .option('-r, --root <path>', 'Data directory')
694
+ .option('-y, --yes', 'Skip the confirmation prompt')
695
+ .option('--purge', 'Also delete pod data at <dataRoot>/<username>/')
696
+ .action(async (username, options) => {
697
+ try {
698
+ if (options.root) {
699
+ process.env.DATA_ROOT = path.resolve(options.root);
700
+ }
701
+
702
+ const account = await findByUsername(username);
703
+ if (!account) {
704
+ console.error(`Error: User not found: ${username}`);
705
+ process.exit(1);
706
+ }
707
+
708
+ if (!options.yes) {
709
+ const summary = `Delete account '${account.username}' (${account.webId})${options.purge ? ' AND purge pod data' : ''}?`;
710
+ const ok = await confirm(summary, false);
711
+ if (!ok) {
712
+ console.log('Cancelled.');
713
+ process.exit(0);
714
+ }
715
+ }
716
+
717
+ await deleteAccount(account.id);
718
+
719
+ if (options.purge) {
720
+ const dataRoot = process.env.DATA_ROOT || './data';
721
+ const podPath = path.join(dataRoot, account.username);
722
+ await fs.remove(podPath);
723
+ console.log(`\nDeleted account ${account.username}. Pod data removed from ${podPath}.\n`);
724
+ } else {
725
+ console.log(`\nDeleted account ${account.username}. Pod data preserved at <dataRoot>/${account.username}/ (use --purge to remove).\n`);
699
726
  }
700
727
  } catch (err) {
701
728
  console.error(`Error: ${err.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.143",
3
+ "version": "0.0.145",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/utils/url.js CHANGED
@@ -239,6 +239,16 @@ export function getContentType(filePath) {
239
239
  '.m3u8': 'application/vnd.apple.mpegurl',
240
240
  '.pls': 'audio/x-scpls'
241
241
  };
242
+
243
+ // Solid convention dotfiles (.acl, .meta) are RDF resources. path.extname
244
+ // returns '' for leading-dot names, so the map lookup above misses them;
245
+ // fall back to a basename check and tag them as JSON-LD — the format JSS
246
+ // writes them in via serializeAcl() / createPodStructure(). Content
247
+ // negotiation then handles Turtle-native clients (umai, Soukai-based apps,
248
+ // older Solid tooling) via handleGet's conneg branch.
249
+ const base = path.basename(filePath);
250
+ if (base === '.acl' || base === '.meta') return 'application/ld+json';
251
+
242
252
  return types[ext] || 'application/octet-stream';
243
253
  }
244
254
 
@@ -194,6 +194,73 @@ describe('Content Negotiation (conneg enabled)', () => {
194
194
  'Accept-Post should include text/turtle');
195
195
  });
196
196
  });
197
+
198
+ // Regression coverage for #294 — Solid convention dotfiles (.acl, .meta)
199
+ // were excluded from conneg because getContentType() returned
200
+ // application/octet-stream for them. Turtle-native clients (umai etc.)
201
+ // fetching <container>/.meta got JSON-LD back and errored on parse.
202
+ describe('Solid convention dotfiles (#294)', () => {
203
+ const metaData = {
204
+ '@context': { 'ldp': 'http://www.w3.org/ns/ldp#' },
205
+ '@id': '',
206
+ '@type': 'ldp:BasicContainer'
207
+ };
208
+
209
+ before(async () => {
210
+ // Write a JSON-LD .meta file (the format JSS writes internally).
211
+ await request('/connegtest/public/.meta', {
212
+ method: 'PUT',
213
+ headers: { 'Content-Type': 'application/ld+json' },
214
+ body: JSON.stringify(metaData),
215
+ auth: 'connegtest'
216
+ });
217
+ });
218
+
219
+ it('serves .meta as JSON-LD by default', async () => {
220
+ const res = await request('/connegtest/public/.meta', { auth: 'connegtest' });
221
+ assertStatus(res, 200);
222
+ assertHeaderContains(res, 'Content-Type', 'application/ld+json');
223
+ });
224
+
225
+ it('serves .meta as Turtle when Accept: text/turtle (the umai case)', async () => {
226
+ const res = await request('/connegtest/public/.meta', {
227
+ headers: { 'Accept': 'text/turtle' },
228
+ auth: 'connegtest'
229
+ });
230
+ assertStatus(res, 200);
231
+ assertHeaderContains(res, 'Content-Type', 'text/turtle');
232
+ const turtle = await res.text();
233
+ // First byte after the `@prefix` block must parse as Turtle,
234
+ // not '{' (the bug signature umai hit).
235
+ assert.ok(!turtle.trimStart().startsWith('{'),
236
+ `response looks like JSON, not Turtle: ${turtle.slice(0, 60)}`);
237
+ });
238
+
239
+ it('accepts Turtle PUT to .meta and round-trips to JSON-LD', async () => {
240
+ const turtle = `
241
+ @prefix ldp: <http://www.w3.org/ns/ldp#>.
242
+ <> a ldp:BasicContainer.
243
+ `;
244
+ const putRes = await request('/connegtest/public/.meta', {
245
+ method: 'PUT',
246
+ headers: { 'Content-Type': 'text/turtle' },
247
+ body: turtle,
248
+ auth: 'connegtest'
249
+ });
250
+ assert.ok(putRes.status < 300, `PUT turtle should succeed, got ${putRes.status}`);
251
+
252
+ // Default GET now serves the converted-and-stored JSON-LD.
253
+ const getRes = await request('/connegtest/public/.meta', {
254
+ headers: { 'Accept': 'application/ld+json' },
255
+ auth: 'connegtest'
256
+ });
257
+ assertStatus(getRes, 200);
258
+ assertHeaderContains(getRes, 'Content-Type', 'application/ld+json');
259
+ const body = await getRes.json();
260
+ assert.ok(body['@context'] || body['@graph'] || body['@type'] || body['@id'],
261
+ 'round-tripped JSON-LD should have at least one @-keyword');
262
+ });
263
+ });
197
264
  });
198
265
 
199
266
  describe('Content Negotiation (conneg disabled - default)', () => {
package/test/url.test.js CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { describe, it } from 'node:test';
9
9
  import assert from 'node:assert';
10
- import { getPodName } from '../src/utils/url.js';
10
+ import { getPodName, getContentType } from '../src/utils/url.js';
11
11
 
12
12
  describe('getPodName', () => {
13
13
  describe('subdomain mode', () => {
@@ -73,3 +73,37 @@ describe('getPodName', () => {
73
73
  });
74
74
  });
75
75
  });
76
+
77
+ // Regression coverage for #294 — .acl and .meta must be recognised as RDF
78
+ // resources so content negotiation kicks in for Turtle-native clients.
79
+ describe('getContentType', () => {
80
+ describe('extension-based mapping (existing)', () => {
81
+ it('maps .jsonld → application/ld+json', () => {
82
+ assert.strictEqual(getContentType('/x/card.jsonld'), 'application/ld+json');
83
+ });
84
+ it('maps .ttl → text/turtle', () => {
85
+ assert.strictEqual(getContentType('/x/card.ttl'), 'text/turtle');
86
+ });
87
+ it('falls back to application/octet-stream for unknown extensions', () => {
88
+ assert.strictEqual(getContentType('/x/file.xyz'), 'application/octet-stream');
89
+ });
90
+ });
91
+
92
+ describe('Solid convention dotfiles (#294)', () => {
93
+ it('treats .acl as application/ld+json (the format JSS writes it in)', () => {
94
+ assert.strictEqual(getContentType('/alice/public/.acl'), 'application/ld+json');
95
+ assert.strictEqual(getContentType('.acl'), 'application/ld+json');
96
+ });
97
+
98
+ it('treats .meta as application/ld+json', () => {
99
+ assert.strictEqual(getContentType('/alice/public/.meta'), 'application/ld+json');
100
+ assert.strictEqual(getContentType('.meta'), 'application/ld+json');
101
+ });
102
+
103
+ it('does not mistake non-dotfile paths containing .acl for ACL files', () => {
104
+ // A regular file that happens to have "acl" in its name/path stays
105
+ // classified by extension, not by coincidence.
106
+ assert.strictEqual(getContentType('/alice/notes/my-acl-plan.md'), 'text/markdown');
107
+ });
108
+ });
109
+ });