dignity.js 0.8.0 → 0.8.2

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.
@@ -13300,6 +13300,123 @@ var require_query_replica = __commonJS({
13300
13300
  }
13301
13301
  });
13302
13302
 
13303
+ // src/apps/manifest.js
13304
+ var require_manifest = __commonJS({
13305
+ "src/apps/manifest.js"(exports, module) {
13306
+ var MANIFEST_SCHEMA_VERSION = 1;
13307
+ var ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
13308
+ function isNonEmptyString(value) {
13309
+ return typeof value === "string" && value.trim().length > 0;
13310
+ }
13311
+ function validateStoredCommand(command, index) {
13312
+ const prefix = `storedCommands[${index}]`;
13313
+ if (!command || typeof command !== "object") {
13314
+ return { ok: false, reason: `${prefix} must be an object` };
13315
+ }
13316
+ if (!isNonEmptyString(command.id)) {
13317
+ return { ok: false, reason: `${prefix}.id is required` };
13318
+ }
13319
+ if (!isNonEmptyString(command.collection)) {
13320
+ return { ok: false, reason: `${prefix}.collection is required` };
13321
+ }
13322
+ if (!["create", "update", "delete"].includes(command.kind)) {
13323
+ return { ok: false, reason: `${prefix}.kind must be create, update, or delete` };
13324
+ }
13325
+ if (command.allowedFields !== void 0) {
13326
+ if (!Array.isArray(command.allowedFields) || command.allowedFields.some((f) => !isNonEmptyString(f))) {
13327
+ return { ok: false, reason: `${prefix}.allowedFields must be a string array` };
13328
+ }
13329
+ }
13330
+ return { ok: true };
13331
+ }
13332
+ function validateDignityAppManifest(raw) {
13333
+ if (!raw || typeof raw !== "object") {
13334
+ return { ok: false, reason: "manifest must be an object" };
13335
+ }
13336
+ if (raw.schemaVersion !== void 0 && raw.schemaVersion !== MANIFEST_SCHEMA_VERSION) {
13337
+ return { ok: false, reason: `unsupported schemaVersion: ${raw.schemaVersion}` };
13338
+ }
13339
+ if (!isNonEmptyString(raw.id) || !ID_PATTERN.test(raw.id)) {
13340
+ return { ok: false, reason: "id must match [a-z0-9][a-z0-9._-]{0,63}" };
13341
+ }
13342
+ if (!isNonEmptyString(raw.title)) {
13343
+ return { ok: false, reason: "title is required" };
13344
+ }
13345
+ if (!Array.isArray(raw.collections) || raw.collections.length === 0) {
13346
+ return { ok: false, reason: "collections must be a non-empty string array" };
13347
+ }
13348
+ const collections = [];
13349
+ for (const name of raw.collections) {
13350
+ if (!isNonEmptyString(name)) {
13351
+ return { ok: false, reason: "collections entries must be non-empty strings" };
13352
+ }
13353
+ if (collections.includes(name)) {
13354
+ return { ok: false, reason: `duplicate collection: ${name}` };
13355
+ }
13356
+ collections.push(name.trim());
13357
+ }
13358
+ const storedCommands = Array.isArray(raw.storedCommands) ? raw.storedCommands : [];
13359
+ for (let index = 0; index < storedCommands.length; index += 1) {
13360
+ const result = validateStoredCommand(storedCommands[index], index);
13361
+ if (!result.ok) {
13362
+ return result;
13363
+ }
13364
+ const collection = storedCommands[index].collection;
13365
+ if (!collections.includes(collection)) {
13366
+ return {
13367
+ ok: false,
13368
+ reason: `storedCommands[${index}] references undeclared collection: ${collection}`
13369
+ };
13370
+ }
13371
+ }
13372
+ const allowedCspOrigins = Array.isArray(raw.allowedCspOrigins) ? raw.allowedCspOrigins : [];
13373
+ for (const origin of allowedCspOrigins) {
13374
+ if (!isNonEmptyString(origin) || !origin.startsWith("https://")) {
13375
+ return { ok: false, reason: "allowedCspOrigins entries must be https:// URLs" };
13376
+ }
13377
+ if (/localhost|127\.0\.0\.1/i.test(origin)) {
13378
+ return { ok: false, reason: "localhost origins are not allowed in allowedCspOrigins" };
13379
+ }
13380
+ }
13381
+ const manifest = {
13382
+ schemaVersion: MANIFEST_SCHEMA_VERSION,
13383
+ id: raw.id.trim(),
13384
+ title: raw.title.trim(),
13385
+ description: isNonEmptyString(raw.description) ? raw.description.trim() : "",
13386
+ collections,
13387
+ peerGroupId: isNonEmptyString(raw.peerGroupId) ? raw.peerGroupId.trim() : null,
13388
+ publisherId: isNonEmptyString(raw.publisherId) ? raw.publisherId.trim() : null,
13389
+ storedCommands: storedCommands.map((cmd) => ({
13390
+ id: cmd.id.trim(),
13391
+ collection: cmd.collection.trim(),
13392
+ kind: cmd.kind,
13393
+ allowedFields: Array.isArray(cmd.allowedFields) ? [...cmd.allowedFields] : null,
13394
+ requiresRole: isNonEmptyString(cmd.requiresRole) ? cmd.requiresRole.trim() : null
13395
+ })),
13396
+ allowedCspOrigins: allowedCspOrigins.map((o) => o.trim()),
13397
+ readOnly: storedCommands.length === 0
13398
+ };
13399
+ return { ok: true, manifest };
13400
+ }
13401
+ function collectionAllowed(manifest, collectionName) {
13402
+ return manifest && Array.isArray(manifest.collections) && manifest.collections.includes(collectionName);
13403
+ }
13404
+ function getStoredCommand(manifest, commandId) {
13405
+ if (!manifest || !Array.isArray(manifest.storedCommands)) {
13406
+ return null;
13407
+ }
13408
+ return manifest.storedCommands.find((cmd) => cmd.id === commandId) || null;
13409
+ }
13410
+ module.exports = {
13411
+ MANIFEST_SCHEMA_VERSION,
13412
+ ID_PATTERN,
13413
+ validateDignityAppManifest,
13414
+ collectionAllowed,
13415
+ getStoredCommand
13416
+ };
13417
+ }
13418
+ });
13419
+
13303
13420
  // src/index.js
13304
13421
  var require_index = __commonJS({
13305
13422
  "src/index.js"(exports, module) {
@@ -13364,6 +13481,12 @@ var require_index = __commonJS({
13364
13481
  } = require_peer_group_tiers();
13365
13482
  var { electBulkRelays, DEFAULT_BULK_RELAY_COUNT } = require_bulk_relay();
13366
13483
  var DignityQueryReplica = require_query_replica();
13484
+ var {
13485
+ MANIFEST_SCHEMA_VERSION: DIGNITY_APP_MANIFEST_SCHEMA_VERSION,
13486
+ validateDignityAppManifest,
13487
+ collectionAllowed,
13488
+ getStoredCommand
13489
+ } = require_manifest();
13367
13490
  module.exports = {
13368
13491
  DignityP2P,
13369
13492
  createDefaultSignalingPool,
@@ -13412,7 +13535,11 @@ var require_index = __commonJS({
13412
13535
  filterPeersByTier,
13413
13536
  electBulkRelays,
13414
13537
  DEFAULT_BULK_RELAY_COUNT,
13415
- DignityQueryReplica
13538
+ DignityQueryReplica,
13539
+ DIGNITY_APP_MANIFEST_SCHEMA_VERSION,
13540
+ validateDignityAppManifest,
13541
+ collectionAllowed,
13542
+ getStoredCommand
13416
13543
  };
13417
13544
  }
13418
13545
  });
package/docs/index.html CHANGED
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <meta name="description" content="dignity.js v0.8.0 — REST-like P2P object API for decentralized JavaScript applications." />
6
+ <meta name="description" content="dignity.js v0.8.2 — REST-like P2P object API for decentralized JavaScript applications." />
7
7
  <title>dignity.js · Documentation</title>
8
8
  <link rel="icon" href="./assets/favicon.svg" type="image/svg+xml" />
9
9
  <link rel="icon" href="./favicon.ico" sizes="32x32" />
@@ -16,7 +16,7 @@
16
16
  <a class="site-header__brand" href="#overview">
17
17
  <img src="./assets/dignity-logo.svg" alt="" width="344" height="80" />
18
18
  <!-- <span>dignity.js</span> -->
19
- <span class="site-header__version">v0.8.0</span>
19
+ <span class="site-header__version">v0.8.2</span>
20
20
  </a>
21
21
  <div class="site-header__links">
22
22
  <a href="https://www.npmjs.com/package/dignity.js" target="_blank" rel="noopener noreferrer">npm</a>
@@ -644,8 +644,9 @@ replica.verifyChain(); // hash-chain consistency</code></pre>
644
644
  </div>
645
645
 
646
646
  <h3>Roadmap</h3>
647
- <p><strong>v0.8.0 (current)</strong> — CQRS tiered PeerGroups, domain events, DignityQueryReplica, hash chains, bulk relay, maxHops 64.</p>
648
- <p><strong>v0.7.1</strong> — live docs playground, demo fixes, syntax highlighting.</p>
647
+ <p><strong>v0.8.2 (current)</strong> — Dignity App manifest validation, sandboxed-apps threat model (#101, #108).</p>
648
+ <p><strong>v0.8.1</strong> — openapi-like.json sync with DignityP2P API.</p>
649
+ <p><strong>v0.8.0</strong> — CQRS tiered PeerGroups, domain events, DignityQueryReplica, hash chains, bulk relay, maxHops 64.</p>
649
650
  <p><strong>v0.7.0 (shipped)</strong> — credential-derived keys, identity rotation, PeerGroup hardening, stress harness.</p>
650
651
  <p><strong>v0.6.0 (shipped)</strong> — core gossip, chess spectators, unit + e2e tests.</p>
651
652
  <p><strong>v0.6.x polish</strong> — <code>subscribeObjectFeed</code> wrapper, connection LRU trim, IndexedDB joined-group persistence, React <code>usePeerGroup</code> hook, 50+ node integration test.</p>
@@ -1039,7 +1040,7 @@ npm run example:chess</code></pre>
1039
1040
 
1040
1041
  <footer class="site-footer">
1041
1042
  <p>
1042
- dignity.js v0.8.0 ·
1043
+ dignity.js v0.8.2 ·
1043
1044
  <a href="https://github.com/jose-compu/dignity.js/blob/main/LICENSE">Apache 2.0</a> ·
1044
1045
  <a href="https://github.com/jose-compu/dignity.js">GitHub</a> ·
1045
1046
  <a href="https://www.npmjs.com/package/dignity.js">npm</a>
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "dignity.js",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "REST-like object API over peer-to-peer replication",
5
+ "lifecycle": {
6
+ "start": "attach network message handler and begin receiving",
7
+ "stop": "leave discovery scopes and detach handler"
8
+ },
5
9
  "resources": {
6
10
  "collections/{collection}/{id}": {
7
11
  "create": {
@@ -11,7 +15,8 @@
11
15
  "id": "optional stable id",
12
16
  "collaborators": "optional peer id list",
13
17
  "broadcastScope": "scoped broadcast password namespace",
14
- "connectToPeers": "optional; defaults to collaborators on PeerJS mesh"
18
+ "connectToPeers": "optional; defaults to collaborators on PeerJS mesh",
19
+ "peerGroupId": "optional; auto-publish signed domain event to this PeerGroup (v0.8+, publisher role)"
15
20
  }
16
21
  },
17
22
  "read": {
@@ -43,7 +48,17 @@
43
48
  },
44
49
  "delete": {
45
50
  "method": "remove(collection, id)",
46
- "authorization": "owner-only"
51
+ "authorization": "owner-only",
52
+ "options": {
53
+ "peerGroupId": "optional; auto-publish domain event when publisher (v0.8+)"
54
+ }
55
+ },
56
+ "transferOwnership": {
57
+ "method": "transferOwnership(collection, id, newOwnerId, options)",
58
+ "authorization": "owner-only",
59
+ "options": {
60
+ "keepAsCollaborator": "default true; previous owner stays collaborator"
61
+ }
47
62
  }
48
63
  },
49
64
  "collections/{collection}": {
@@ -57,7 +72,28 @@
57
72
  "getConnectionStats": "{ openCount, peerIds }",
58
73
  "ensureConnectedToPeers": "connect to many peers before broadcast",
59
74
  "joinDiscovery": "scoped presence; options.bootstrapPeerIds connects before announce",
60
- "broadcastMessage": "custom app messages; options.connectToPeers"
75
+ "leaveDiscovery": "stop heartbeat and remove local presence from scope",
76
+ "listPeers": "list presence entries in a discovery scope",
77
+ "announcePresence": "manual presence heartbeat for a joined scope",
78
+ "broadcastMessage": "custom app messages; options.connectToPeers",
79
+ "sendDirectMessage": "encrypted direct message to targetId",
80
+ "registerPeerPublicKey": "trust peer signing/encryption keys with optional generation",
81
+ "trustPeerPublicKey": "register trusted keys without generation metadata",
82
+ "getPublicKey": "returns this node's public key bundle",
83
+ "unbanPeer": "clear manual or automatic peer ban",
84
+ "getBanInfo": "returns ban expiry and reason for peerId, or null"
85
+ },
86
+ "identity": {
87
+ "getPeerIdentityGeneration": "trusted identity generation for peerId",
88
+ "getPeerIdentityState": "public key + generation state for peerId",
89
+ "adoptDerivedIdentityKeyPair": "install credential-derived keys locally",
90
+ "deriveAndAdoptIdentity": "derive keys from username/password and adopt",
91
+ "broadcastIdentityRotation": "broadcast signed identity:rotate to peers",
92
+ "enrollAndBroadcastColdRecovery": "enroll cold recovery key and announce",
93
+ "revokeAndRotateDerivedIdentity": "compromise recovery rotation helper",
94
+ "rotateDerivedIdentityPassword": "password-change rotation helper",
95
+ "applyPeerIdentityRotation": "apply remote identity:rotate message",
96
+ "applyPeerColdRecoveryEnrollment": "apply remote cold-recovery enrollment"
61
97
  },
62
98
  "peerGroups": {
63
99
  "joinPeerGroup": "join scalable gossip group; options: fanout, maxActivePeers, maxHops (default 64), role, tiered, liveCap, tierMode, domainEvents, publisherId",
@@ -86,8 +122,12 @@
86
122
  "bulkrelaychanged": "bulk relay peer set changed for a tiered group",
87
123
  "checkpointpublished": "publisher published domain-event checkpoint",
88
124
  "peerdiscovered": "peer joined discovery scope",
125
+ "peergroupjoined": "local node joined a PeerGroup",
126
+ "peergroupleft": "local node left a PeerGroup",
89
127
  "peerleft": "peer left or timed out",
90
- "message": "custom decrypted message received"
128
+ "message": "custom decrypted message received",
129
+ "securityerror": "signature, PoW, or decrypt failure on incoming message",
130
+ "messageignored": "message dropped (wrong target, banned peer, etc.)"
91
131
  },
92
132
  "recordShape": {
93
133
  "active": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dignity.js",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "P2P object API for decentralized JavaScript applications",
5
5
  "homepage": "https://jose-compu.github.io/dignity.js/",
6
6
  "repository": {
@@ -0,0 +1,131 @@
1
+ const MANIFEST_SCHEMA_VERSION = 1;
2
+ const ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
3
+
4
+ function isNonEmptyString(value) {
5
+ return typeof value === 'string' && value.trim().length > 0;
6
+ }
7
+
8
+ function validateStoredCommand(command, index) {
9
+ const prefix = `storedCommands[${index}]`;
10
+ if (!command || typeof command !== 'object') {
11
+ return { ok: false, reason: `${prefix} must be an object` };
12
+ }
13
+ if (!isNonEmptyString(command.id)) {
14
+ return { ok: false, reason: `${prefix}.id is required` };
15
+ }
16
+ if (!isNonEmptyString(command.collection)) {
17
+ return { ok: false, reason: `${prefix}.collection is required` };
18
+ }
19
+ if (!['create', 'update', 'delete'].includes(command.kind)) {
20
+ return { ok: false, reason: `${prefix}.kind must be create, update, or delete` };
21
+ }
22
+ if (command.allowedFields !== undefined) {
23
+ if (!Array.isArray(command.allowedFields) || command.allowedFields.some((f) => !isNonEmptyString(f))) {
24
+ return { ok: false, reason: `${prefix}.allowedFields must be a string array` };
25
+ }
26
+ }
27
+ return { ok: true };
28
+ }
29
+
30
+ /**
31
+ * Validate a Dignity App manifest (issue #101).
32
+ * @returns {{ ok: true, manifest: object } | { ok: false, reason: string }}
33
+ */
34
+ function validateDignityAppManifest(raw) {
35
+ if (!raw || typeof raw !== 'object') {
36
+ return { ok: false, reason: 'manifest must be an object' };
37
+ }
38
+
39
+ if (raw.schemaVersion !== undefined && raw.schemaVersion !== MANIFEST_SCHEMA_VERSION) {
40
+ return { ok: false, reason: `unsupported schemaVersion: ${raw.schemaVersion}` };
41
+ }
42
+
43
+ if (!isNonEmptyString(raw.id) || !ID_PATTERN.test(raw.id)) {
44
+ return { ok: false, reason: 'id must match [a-z0-9][a-z0-9._-]{0,63}' };
45
+ }
46
+
47
+ if (!isNonEmptyString(raw.title)) {
48
+ return { ok: false, reason: 'title is required' };
49
+ }
50
+
51
+ if (!Array.isArray(raw.collections) || raw.collections.length === 0) {
52
+ return { ok: false, reason: 'collections must be a non-empty string array' };
53
+ }
54
+
55
+ const collections = [];
56
+ for (const name of raw.collections) {
57
+ if (!isNonEmptyString(name)) {
58
+ return { ok: false, reason: 'collections entries must be non-empty strings' };
59
+ }
60
+ if (collections.includes(name)) {
61
+ return { ok: false, reason: `duplicate collection: ${name}` };
62
+ }
63
+ collections.push(name.trim());
64
+ }
65
+
66
+ const storedCommands = Array.isArray(raw.storedCommands) ? raw.storedCommands : [];
67
+ for (let index = 0; index < storedCommands.length; index += 1) {
68
+ const result = validateStoredCommand(storedCommands[index], index);
69
+ if (!result.ok) {
70
+ return result;
71
+ }
72
+ const collection = storedCommands[index].collection;
73
+ if (!collections.includes(collection)) {
74
+ return {
75
+ ok: false,
76
+ reason: `storedCommands[${index}] references undeclared collection: ${collection}`
77
+ };
78
+ }
79
+ }
80
+
81
+ const allowedCspOrigins = Array.isArray(raw.allowedCspOrigins) ? raw.allowedCspOrigins : [];
82
+ for (const origin of allowedCspOrigins) {
83
+ if (!isNonEmptyString(origin) || !origin.startsWith('https://')) {
84
+ return { ok: false, reason: 'allowedCspOrigins entries must be https:// URLs' };
85
+ }
86
+ if (/localhost|127\.0\.0\.1/i.test(origin)) {
87
+ return { ok: false, reason: 'localhost origins are not allowed in allowedCspOrigins' };
88
+ }
89
+ }
90
+
91
+ const manifest = {
92
+ schemaVersion: MANIFEST_SCHEMA_VERSION,
93
+ id: raw.id.trim(),
94
+ title: raw.title.trim(),
95
+ description: isNonEmptyString(raw.description) ? raw.description.trim() : '',
96
+ collections,
97
+ peerGroupId: isNonEmptyString(raw.peerGroupId) ? raw.peerGroupId.trim() : null,
98
+ publisherId: isNonEmptyString(raw.publisherId) ? raw.publisherId.trim() : null,
99
+ storedCommands: storedCommands.map((cmd) => ({
100
+ id: cmd.id.trim(),
101
+ collection: cmd.collection.trim(),
102
+ kind: cmd.kind,
103
+ allowedFields: Array.isArray(cmd.allowedFields) ? [...cmd.allowedFields] : null,
104
+ requiresRole: isNonEmptyString(cmd.requiresRole) ? cmd.requiresRole.trim() : null
105
+ })),
106
+ allowedCspOrigins: allowedCspOrigins.map((o) => o.trim()),
107
+ readOnly: storedCommands.length === 0
108
+ };
109
+
110
+ return { ok: true, manifest };
111
+ }
112
+
113
+ function collectionAllowed(manifest, collectionName) {
114
+ return manifest && Array.isArray(manifest.collections)
115
+ && manifest.collections.includes(collectionName);
116
+ }
117
+
118
+ function getStoredCommand(manifest, commandId) {
119
+ if (!manifest || !Array.isArray(manifest.storedCommands)) {
120
+ return null;
121
+ }
122
+ return manifest.storedCommands.find((cmd) => cmd.id === commandId) || null;
123
+ }
124
+
125
+ module.exports = {
126
+ MANIFEST_SCHEMA_VERSION,
127
+ ID_PATTERN,
128
+ validateDignityAppManifest,
129
+ collectionAllowed,
130
+ getStoredCommand
131
+ };
package/src/index.js CHANGED
@@ -67,6 +67,12 @@ const {
67
67
  } = require('./cqrs/peer-group-tiers');
68
68
  const { electBulkRelays, DEFAULT_BULK_RELAY_COUNT } = require('./cqrs/bulk-relay');
69
69
  const DignityQueryReplica = require('./cqrs/query-replica');
70
+ const {
71
+ MANIFEST_SCHEMA_VERSION: DIGNITY_APP_MANIFEST_SCHEMA_VERSION,
72
+ validateDignityAppManifest,
73
+ collectionAllowed,
74
+ getStoredCommand
75
+ } = require('./apps/manifest');
70
76
 
71
77
  module.exports = {
72
78
  DignityP2P,
@@ -116,5 +122,9 @@ module.exports = {
116
122
  filterPeersByTier,
117
123
  electBulkRelays,
118
124
  DEFAULT_BULK_RELAY_COUNT,
119
- DignityQueryReplica
125
+ DignityQueryReplica,
126
+ DIGNITY_APP_MANIFEST_SCHEMA_VERSION,
127
+ validateDignityAppManifest,
128
+ collectionAllowed,
129
+ getStoredCommand
120
130
  };