dignity.js 0.8.1 → 0.8.3
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/README.md +26 -0
- package/dist/dignity.cjs.js +128 -1
- package/dist/dignity.cjs.js.map +4 -4
- package/dist/dignity.esm.js +128 -1
- package/dist/dignity.esm.js.map +3 -3
- package/dist/dignity.min.js +26 -26
- package/docs/assets/dignity.esm.js +128 -1
- package/docs/index.html +121 -7
- package/docs/openapi-like.json +1 -1
- package/package.json +5 -2
- package/src/apps/manifest.js +131 -0
- package/src/index.js +11 -1
|
@@ -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.
|
|
6
|
+
<meta name="description" content="dignity.js v0.8.3 — 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.
|
|
19
|
+
<span class="site-header__version">v0.8.3</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>
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
<a href="./playground/index.html">Playground</a>
|
|
25
25
|
<a href="./chess/index.html">3D Chess</a>
|
|
26
26
|
<a href="./openapi-like.json">API JSON</a>
|
|
27
|
+
<a href="./api-reference.md">API MD</a>
|
|
27
28
|
</div>
|
|
28
29
|
<button class="menu-toggle" type="button" aria-label="Toggle navigation">Menu</button>
|
|
29
30
|
</header>
|
|
@@ -62,6 +63,9 @@
|
|
|
62
63
|
<div class="sidebar__label">Reference</div>
|
|
63
64
|
<nav>
|
|
64
65
|
<a href="#api-reference">API reference</a>
|
|
66
|
+
<a href="./api-reference.md">Full API (Markdown)</a>
|
|
67
|
+
<a href="#browser-compat">Browser compatibility</a>
|
|
68
|
+
<a href="#benchmarks">Benchmarks</a>
|
|
65
69
|
<a href="#events">Events</a>
|
|
66
70
|
<a href="#exports">Package exports</a>
|
|
67
71
|
<a href="#examples">Examples</a>
|
|
@@ -101,6 +105,10 @@
|
|
|
101
105
|
<h3>CQRS PeerGroups (v0.8+)</h3>
|
|
102
106
|
<p>Live + bulk tiers, signed domain events, and <code>DignityQueryReplica</code> read views for massive audiences.</p>
|
|
103
107
|
</article>
|
|
108
|
+
<article class="card">
|
|
109
|
+
<h3>Reference & benchmarks (v0.8.3)</h3>
|
|
110
|
+
<p><a href="./api-reference.md">API reference</a>, <a href="./browser-compatibility.md">browser matrix</a>, and <a href="./benchmarks/results.json">performance numbers</a>.</p>
|
|
111
|
+
</article>
|
|
104
112
|
</div>
|
|
105
113
|
</section>
|
|
106
114
|
|
|
@@ -644,8 +652,10 @@ replica.verifyChain(); // hash-chain consistency</code></pre>
|
|
|
644
652
|
</div>
|
|
645
653
|
|
|
646
654
|
<h3>Roadmap</h3>
|
|
647
|
-
<p><strong>v0.8.
|
|
648
|
-
<p><strong>v0.
|
|
655
|
+
<p><strong>v0.8.3 (current)</strong> — API reference (#93), browser compatibility matrix (#91), performance benchmarks (#92).</p>
|
|
656
|
+
<p><strong>v0.8.2</strong> — Dignity App manifest validation, sandboxed-apps threat model (#101, #108).</p>
|
|
657
|
+
<p><strong>v0.8.1</strong> — openapi-like.json sync with DignityP2P API.</p>
|
|
658
|
+
<p><strong>v0.8.0</strong> — CQRS tiered PeerGroups, domain events, DignityQueryReplica, hash chains, bulk relay, maxHops 64.</p>
|
|
649
659
|
<p><strong>v0.7.0 (shipped)</strong> — credential-derived keys, identity rotation, PeerGroup hardening, stress harness.</p>
|
|
650
660
|
<p><strong>v0.6.0 (shipped)</strong> — core gossip, chess spectators, unit + e2e tests.</p>
|
|
651
661
|
<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>
|
|
@@ -907,9 +917,92 @@ const pool = createDefaultSignalingPool({
|
|
|
907
917
|
<p>
|
|
908
918
|
Machine-readable metadata:
|
|
909
919
|
<a href="./openapi-like.json">openapi-like.json</a>
|
|
920
|
+
·
|
|
921
|
+
<a href="./api-reference.md">api-reference.md</a> (generated, #93)
|
|
910
922
|
</p>
|
|
911
923
|
</section>
|
|
912
924
|
|
|
925
|
+
<section id="browser-compat" class="section">
|
|
926
|
+
<h2>Browser compatibility</h2>
|
|
927
|
+
<p>
|
|
928
|
+
dignity.js requires WebRTC, IndexedDB (optional persistence), and Web Crypto in a secure context (HTTPS or localhost).
|
|
929
|
+
CI runs Node.js 18/20/22 only — browser rows reflect manual smoke tests on the playground and chess demo.
|
|
930
|
+
Full matrix: <a href="./browser-compatibility.md">browser-compatibility.md</a> (#91).
|
|
931
|
+
</p>
|
|
932
|
+
<div class="table-wrap">
|
|
933
|
+
<table>
|
|
934
|
+
<thead>
|
|
935
|
+
<tr>
|
|
936
|
+
<th>Browser</th>
|
|
937
|
+
<th>Status</th>
|
|
938
|
+
</tr>
|
|
939
|
+
</thead>
|
|
940
|
+
<tbody>
|
|
941
|
+
<tr><td>Chrome / Chromium</td><td>Supported (primary dev target)</td></tr>
|
|
942
|
+
<tr><td>Firefox</td><td>Supported</td></tr>
|
|
943
|
+
<tr><td>Safari</td><td>Supported with WebRTC / background-tab caveats</td></tr>
|
|
944
|
+
<tr><td>Edge (Chromium)</td><td>Supported</td></tr>
|
|
945
|
+
<tr><td>Node.js 18+</td><td>Supported (in-memory / tests)</td></tr>
|
|
946
|
+
</tbody>
|
|
947
|
+
</table>
|
|
948
|
+
</div>
|
|
949
|
+
</section>
|
|
950
|
+
|
|
951
|
+
<section id="benchmarks" class="section">
|
|
952
|
+
<h2>Performance benchmarks</h2>
|
|
953
|
+
<p>
|
|
954
|
+
Reproducible in-memory gossip latency and IndexedDB hydration numbers (#92).
|
|
955
|
+
Regenerate: <code>npm run benchmark</code> · smoke: <code>npm run benchmark:quick</code>.
|
|
956
|
+
Details: <a href="./benchmarks/README.md">benchmarks/README.md</a>.
|
|
957
|
+
</p>
|
|
958
|
+
<p>Latest checked-in results: <a href="./benchmarks/results.json">benchmarks/results.json</a> (Node in-memory harness; not real WebRTC).</p>
|
|
959
|
+
<div class="table-wrap">
|
|
960
|
+
<table>
|
|
961
|
+
<thead>
|
|
962
|
+
<tr>
|
|
963
|
+
<th>Suite</th>
|
|
964
|
+
<th>Audience / records</th>
|
|
965
|
+
<th>p50</th>
|
|
966
|
+
<th>p90</th>
|
|
967
|
+
<th>p99</th>
|
|
968
|
+
<th>Delivery</th>
|
|
969
|
+
</tr>
|
|
970
|
+
</thead>
|
|
971
|
+
<tbody>
|
|
972
|
+
<tr>
|
|
973
|
+
<td>Gossip epidemic</td>
|
|
974
|
+
<td>100 subscribers</td>
|
|
975
|
+
<td>2 ms</td>
|
|
976
|
+
<td>5 ms</td>
|
|
977
|
+
<td>5 ms</td>
|
|
978
|
+
<td>93%</td>
|
|
979
|
+
</tr>
|
|
980
|
+
<tr>
|
|
981
|
+
<td>CQRS tiered gossip</td>
|
|
982
|
+
<td>50 subs, liveCap 20</td>
|
|
983
|
+
<td>1 ms</td>
|
|
984
|
+
<td>1 ms</td>
|
|
985
|
+
<td>1 ms</td>
|
|
986
|
+
<td>100%</td>
|
|
987
|
+
</tr>
|
|
988
|
+
<tr>
|
|
989
|
+
<td>IndexedDB hydrate</td>
|
|
990
|
+
<td>1,000 records</td>
|
|
991
|
+
<td colspan="3">6 ms total (~0.006 ms/record)</td>
|
|
992
|
+
<td>—</td>
|
|
993
|
+
</tr>
|
|
994
|
+
<tr>
|
|
995
|
+
<td>IndexedDB hydrate</td>
|
|
996
|
+
<td>2,000 records</td>
|
|
997
|
+
<td colspan="3">17 ms total (~0.009 ms/record)</td>
|
|
998
|
+
<td>—</td>
|
|
999
|
+
</tr>
|
|
1000
|
+
</tbody>
|
|
1001
|
+
</table>
|
|
1002
|
+
</div>
|
|
1003
|
+
<p><small>Figures from <code>docs/benchmarks/results.json</code>; re-run locally to refresh after harness changes.</small></p>
|
|
1004
|
+
</section>
|
|
1005
|
+
|
|
913
1006
|
<section id="events" class="section">
|
|
914
1007
|
<h2>Events</h2>
|
|
915
1008
|
<p><code>DignityP2P</code> extends <code>EventEmitter</code>. Common events:</p>
|
|
@@ -1008,9 +1101,24 @@ const pool = createDefaultSignalingPool({
|
|
|
1008
1101
|
</tr>
|
|
1009
1102
|
<tr>
|
|
1010
1103
|
<td><a href="./chess/index.html"><code>docs/chess/</code></a></td>
|
|
1011
|
-
<td>3D browser chess — PeerJS mesh, dual-signed resume links, IndexedDB, React hooks</td>
|
|
1104
|
+
<td>3D browser chess — PeerJS mesh, dual-signed resume links, IndexedDB, React hooks (v0.8.3)</td>
|
|
1012
1105
|
<td><code>npm run build:chess</code> then <code>npm run docs:serve</code></td>
|
|
1013
1106
|
</tr>
|
|
1107
|
+
<tr>
|
|
1108
|
+
<td><a href="./api-reference.md"><code>docs/api-reference.md</code></a></td>
|
|
1109
|
+
<td>Generated API reference from <code>openapi-like.json</code> (#93)</td>
|
|
1110
|
+
<td><code>npm run docs:api-reference</code></td>
|
|
1111
|
+
</tr>
|
|
1112
|
+
<tr>
|
|
1113
|
+
<td><a href="./browser-compatibility.md"><code>docs/browser-compatibility.md</code></a></td>
|
|
1114
|
+
<td>Browser support matrix and platform caveats (#91)</td>
|
|
1115
|
+
<td>—</td>
|
|
1116
|
+
</tr>
|
|
1117
|
+
<tr>
|
|
1118
|
+
<td><a href="./benchmarks/results.json"><code>docs/benchmarks/</code></a></td>
|
|
1119
|
+
<td>Gossip latency and IndexedDB hydration benchmarks (#92)</td>
|
|
1120
|
+
<td><code>npm run benchmark</code></td>
|
|
1121
|
+
</tr>
|
|
1014
1122
|
</tbody>
|
|
1015
1123
|
</table>
|
|
1016
1124
|
</div>
|
|
@@ -1019,7 +1127,7 @@ const pool = createDefaultSignalingPool({
|
|
|
1019
1127
|
<section id="development" class="section">
|
|
1020
1128
|
<h2>Development</h2>
|
|
1021
1129
|
<div class="code-block">
|
|
1022
|
-
<pre><code class="language-bash"># Run tests (
|
|
1130
|
+
<pre><code class="language-bash"># Run tests (291+ passing)
|
|
1023
1131
|
npm test
|
|
1024
1132
|
|
|
1025
1133
|
# PeerGroup + CQRS unit + integration tests
|
|
@@ -1028,6 +1136,12 @@ npm run test:peer-group
|
|
|
1028
1136
|
# Build browser + Node bundles
|
|
1029
1137
|
npm run build
|
|
1030
1138
|
|
|
1139
|
+
# Regenerate API reference markdown (#93)
|
|
1140
|
+
npm run docs:api-reference
|
|
1141
|
+
|
|
1142
|
+
# Run performance benchmarks (#92)
|
|
1143
|
+
npm run benchmark
|
|
1144
|
+
|
|
1031
1145
|
# Serve docs locally
|
|
1032
1146
|
npm run docs:serve
|
|
1033
1147
|
|
|
@@ -1039,7 +1153,7 @@ npm run example:chess</code></pre>
|
|
|
1039
1153
|
|
|
1040
1154
|
<footer class="site-footer">
|
|
1041
1155
|
<p>
|
|
1042
|
-
dignity.js v0.8.
|
|
1156
|
+
dignity.js v0.8.3 ·
|
|
1043
1157
|
<a href="https://github.com/jose-compu/dignity.js/blob/main/LICENSE">Apache 2.0</a> ·
|
|
1044
1158
|
<a href="https://github.com/jose-compu/dignity.js">GitHub</a> ·
|
|
1045
1159
|
<a href="https://www.npmjs.com/package/dignity.js">npm</a>
|
package/docs/openapi-like.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dignity.js",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "P2P object API for decentralized JavaScript applications",
|
|
5
5
|
"homepage": "https://jose-compu.github.io/dignity.js/",
|
|
6
6
|
"repository": {
|
|
@@ -53,7 +53,10 @@
|
|
|
53
53
|
"docs:dev": "node scripts/serve-docs.js",
|
|
54
54
|
"docs:serve": "node scripts/serve-docs.js",
|
|
55
55
|
"docs:stop": "node scripts/stop-docs.js",
|
|
56
|
-
"docs:
|
|
56
|
+
"docs:api-reference": "node scripts/generate-api-reference.js",
|
|
57
|
+
"benchmark": "node scripts/run-benchmarks.js --write docs/benchmarks/results.json",
|
|
58
|
+
"benchmark:quick": "node scripts/run-benchmarks.js --quick",
|
|
59
|
+
"docs:check": "node -e \"const fs=require('fs');['docs/index.html','docs/favicon.ico','docs/api-reference.md','docs/browser-compatibility.md','docs/benchmarks/results.json','docs/playground/index.html','docs/assets/playground.js','docs/assets/playground-demos.js','docs/assets/playground.css','docs/chess/index.html','docs/chess/favicon.ico','docs/assets/favicon.svg','docs/chess/assets/chess-app.js','docs/assets/dignity.esm.js','docs/assets/highlight/highlight.min.js','docs/assets/highlight/github.min.css','docs/assets/highlight/github-dark.min.css'].forEach(p=>fs.accessSync(p));\"",
|
|
57
60
|
"example:tictactoe": "node examples/decentralized-tictactoe.js",
|
|
58
61
|
"example:chess": "node examples/decentralized-chess-lite.js",
|
|
59
62
|
"prepublishOnly": "npm test && npm run build"
|
|
@@ -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
|
};
|