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.
- package/README.md +38 -4
- 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 +6 -5
- package/docs/openapi-like.json +45 -5
- package/package.json +1 -1
- package/src/apps/manifest.js +131 -0
- package/src/index.js +11 -1
package/README.md
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
[](https://www.npmjs.com/package/dignity.js)
|
|
7
7
|
[](https://www.npmjs.com/package/dignity.js)
|
|
8
8
|
[](https://github.com/jose-compu/dignity.js/actions/workflows/ci.yml)
|
|
9
|
-

|
|
10
10
|

|
|
11
11
|

|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
The Scalable Data Layer of the Decentralized Browser Application Ecosystem.
|
|
14
14
|
|
|
15
15
|
`dignity.js` lets many browsers synchronize shared objects with ownership rules and built-in anti-abuse + privacy controls.
|
|
16
16
|
|
|
@@ -33,6 +33,10 @@ REST-like P2P object API for decentralized JavaScript applications.
|
|
|
33
33
|
- Auto `connectToPeers` on create/update/delete replication (owner + collaborators)
|
|
34
34
|
- Optional IndexedDB persistence for browser reload survival
|
|
35
35
|
- Optional React hooks via `dignity.js/react`
|
|
36
|
+
- **PeerGroup gossip** — scalable PubSub for high-fanout feeds (spectators, timelines); default `maxHops: 64`
|
|
37
|
+
- **CQRS tiers (v0.8+)** — live core (5k cap) + bulk tail per publisher; signed domain events on every write
|
|
38
|
+
- **`DignityQueryReplica`** — read-only materialized views with hash-chain verification
|
|
39
|
+
- Credential-derived keys, identity rotation, and cold-recovery co-sign (v0.7+)
|
|
36
40
|
- Browser-first: published npm package includes IIFE, ESM, and CJS builds
|
|
37
41
|
|
|
38
42
|
## Install
|
|
@@ -133,13 +137,16 @@ For high-fanout object updates (millions of subscribers per published object), u
|
|
|
133
137
|
await node.joinPeerGroup('feed:alice', {
|
|
134
138
|
bootstrapPeerIds: ['publisher-peer-id'],
|
|
135
139
|
fanout: 3,
|
|
136
|
-
maxActivePeers: 8
|
|
140
|
+
maxActivePeers: 8,
|
|
141
|
+
maxHops: 64 // default since v0.8.0
|
|
137
142
|
});
|
|
138
143
|
|
|
139
144
|
await node.publishRecordToPeerGroup('feed:alice', 'posts', 'post-1');
|
|
140
145
|
await node.leavePeerGroup('feed:alice');
|
|
141
146
|
```
|
|
142
147
|
|
|
148
|
+
Inner gossip message types: `operation`, `record:snapshot`, `domain:event`, `domain:checkpoint`, and app-defined payloads (via `peergroupmessage` events).
|
|
149
|
+
|
|
143
150
|
Small collaborations (chess players, document co-editing) should keep using direct `connectToPeers` mesh. Large read-only audiences (chess spectators, public timelines) should use PeerGroup gossip. See the [docs PeerGroup section](https://jose-compu.github.io/dignity.js/#peer-groups).
|
|
144
151
|
|
|
145
152
|
### CQRS tiers, domain events, and query replicas (v0.8+)
|
|
@@ -174,7 +181,11 @@ replica.read('posts', 'p1');
|
|
|
174
181
|
replica.verifyChain(); // hash-chain consistency
|
|
175
182
|
```
|
|
176
183
|
|
|
177
|
-
Default `maxHops` is **64** (was 6), sufficient for epidemic spread at fanout 3 without per-group tuning.
|
|
184
|
+
Default `maxHops` is **64** (was 6 in v0.7.x), sufficient for epidemic spread at fanout 3 without per-group tuning.
|
|
185
|
+
|
|
186
|
+
**Publisher options:** `role: 'publisher'`, `tiered: true`, `liveCap` (default 5000), `domainEvents: true`, `peerGroupId` on CRUD to auto-publish events.
|
|
187
|
+
|
|
188
|
+
**Subscriber / replica options:** `role: 'subscriber'`, `tierMode: 'auto' | 'live' | 'bulk'`, `commandCapable: false` on read-only nodes, `publisherId` to filter events.
|
|
178
189
|
|
|
179
190
|
## Room / Team Discovery
|
|
180
191
|
|
|
@@ -508,6 +519,29 @@ npm run docs:dev
|
|
|
508
519
|
|
|
509
520
|
If 4173 is busy, `docs:dev` auto-picks the next free port (4174, 4175, …) and prints the URLs.
|
|
510
521
|
|
|
522
|
+
## Dignity Apps (v0.8.2+)
|
|
523
|
+
|
|
524
|
+
Self-contained HTML apps in a sandboxed iframe, inspired by [Datasette Apps](https://datasette.io/blog/2026/datasette-apps/). Track: [#100](https://github.com/jose-compu/dignity.js/issues/100).
|
|
525
|
+
|
|
526
|
+
**Threat boundaries (v0.8.2):**
|
|
527
|
+
|
|
528
|
+
- Apps run in an iframe with `sandbox` + immutable CSP — no parent DOM, cookies, or `localStorage`.
|
|
529
|
+
- Data access only via a parent **MessageChannel** bridge; no signing keys or mesh credentials in the iframe.
|
|
530
|
+
- **Read:** `dignity.query` backed by `DignityQueryReplica` (collections allowlisted in manifest).
|
|
531
|
+
- **Write:** only **stored commands** pre-declared in the app manifest — no arbitrary CRUD.
|
|
532
|
+
- External `fetch` blocked unless origin is listed in `allowedCspOrigins` (https only; no localhost).
|
|
533
|
+
|
|
534
|
+
```js
|
|
535
|
+
const { validateDignityAppManifest } = require('dignity.js');
|
|
536
|
+
|
|
537
|
+
const { ok, manifest } = validateDignityAppManifest({
|
|
538
|
+
id: 'timeline-demo',
|
|
539
|
+
title: 'Event timeline',
|
|
540
|
+
collections: ['posts'],
|
|
541
|
+
peerGroupId: 'feed:alice'
|
|
542
|
+
});
|
|
543
|
+
```
|
|
544
|
+
|
|
511
545
|
## Docs and Examples
|
|
512
546
|
|
|
513
547
|
- **Documentation:** [jose-compu.github.io/dignity.js](https://jose-compu.github.io/dignity.js/)
|
package/dist/dignity.cjs.js
CHANGED
|
@@ -13270,6 +13270,123 @@ var require_query_replica = __commonJS({
|
|
|
13270
13270
|
}
|
|
13271
13271
|
});
|
|
13272
13272
|
|
|
13273
|
+
// src/apps/manifest.js
|
|
13274
|
+
var require_manifest = __commonJS({
|
|
13275
|
+
"src/apps/manifest.js"(exports2, module2) {
|
|
13276
|
+
var MANIFEST_SCHEMA_VERSION = 1;
|
|
13277
|
+
var ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
13278
|
+
function isNonEmptyString(value) {
|
|
13279
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
13280
|
+
}
|
|
13281
|
+
function validateStoredCommand(command, index) {
|
|
13282
|
+
const prefix = `storedCommands[${index}]`;
|
|
13283
|
+
if (!command || typeof command !== "object") {
|
|
13284
|
+
return { ok: false, reason: `${prefix} must be an object` };
|
|
13285
|
+
}
|
|
13286
|
+
if (!isNonEmptyString(command.id)) {
|
|
13287
|
+
return { ok: false, reason: `${prefix}.id is required` };
|
|
13288
|
+
}
|
|
13289
|
+
if (!isNonEmptyString(command.collection)) {
|
|
13290
|
+
return { ok: false, reason: `${prefix}.collection is required` };
|
|
13291
|
+
}
|
|
13292
|
+
if (!["create", "update", "delete"].includes(command.kind)) {
|
|
13293
|
+
return { ok: false, reason: `${prefix}.kind must be create, update, or delete` };
|
|
13294
|
+
}
|
|
13295
|
+
if (command.allowedFields !== void 0) {
|
|
13296
|
+
if (!Array.isArray(command.allowedFields) || command.allowedFields.some((f) => !isNonEmptyString(f))) {
|
|
13297
|
+
return { ok: false, reason: `${prefix}.allowedFields must be a string array` };
|
|
13298
|
+
}
|
|
13299
|
+
}
|
|
13300
|
+
return { ok: true };
|
|
13301
|
+
}
|
|
13302
|
+
function validateDignityAppManifest2(raw) {
|
|
13303
|
+
if (!raw || typeof raw !== "object") {
|
|
13304
|
+
return { ok: false, reason: "manifest must be an object" };
|
|
13305
|
+
}
|
|
13306
|
+
if (raw.schemaVersion !== void 0 && raw.schemaVersion !== MANIFEST_SCHEMA_VERSION) {
|
|
13307
|
+
return { ok: false, reason: `unsupported schemaVersion: ${raw.schemaVersion}` };
|
|
13308
|
+
}
|
|
13309
|
+
if (!isNonEmptyString(raw.id) || !ID_PATTERN.test(raw.id)) {
|
|
13310
|
+
return { ok: false, reason: "id must match [a-z0-9][a-z0-9._-]{0,63}" };
|
|
13311
|
+
}
|
|
13312
|
+
if (!isNonEmptyString(raw.title)) {
|
|
13313
|
+
return { ok: false, reason: "title is required" };
|
|
13314
|
+
}
|
|
13315
|
+
if (!Array.isArray(raw.collections) || raw.collections.length === 0) {
|
|
13316
|
+
return { ok: false, reason: "collections must be a non-empty string array" };
|
|
13317
|
+
}
|
|
13318
|
+
const collections = [];
|
|
13319
|
+
for (const name of raw.collections) {
|
|
13320
|
+
if (!isNonEmptyString(name)) {
|
|
13321
|
+
return { ok: false, reason: "collections entries must be non-empty strings" };
|
|
13322
|
+
}
|
|
13323
|
+
if (collections.includes(name)) {
|
|
13324
|
+
return { ok: false, reason: `duplicate collection: ${name}` };
|
|
13325
|
+
}
|
|
13326
|
+
collections.push(name.trim());
|
|
13327
|
+
}
|
|
13328
|
+
const storedCommands = Array.isArray(raw.storedCommands) ? raw.storedCommands : [];
|
|
13329
|
+
for (let index = 0; index < storedCommands.length; index += 1) {
|
|
13330
|
+
const result = validateStoredCommand(storedCommands[index], index);
|
|
13331
|
+
if (!result.ok) {
|
|
13332
|
+
return result;
|
|
13333
|
+
}
|
|
13334
|
+
const collection = storedCommands[index].collection;
|
|
13335
|
+
if (!collections.includes(collection)) {
|
|
13336
|
+
return {
|
|
13337
|
+
ok: false,
|
|
13338
|
+
reason: `storedCommands[${index}] references undeclared collection: ${collection}`
|
|
13339
|
+
};
|
|
13340
|
+
}
|
|
13341
|
+
}
|
|
13342
|
+
const allowedCspOrigins = Array.isArray(raw.allowedCspOrigins) ? raw.allowedCspOrigins : [];
|
|
13343
|
+
for (const origin of allowedCspOrigins) {
|
|
13344
|
+
if (!isNonEmptyString(origin) || !origin.startsWith("https://")) {
|
|
13345
|
+
return { ok: false, reason: "allowedCspOrigins entries must be https:// URLs" };
|
|
13346
|
+
}
|
|
13347
|
+
if (/localhost|127\.0\.0\.1/i.test(origin)) {
|
|
13348
|
+
return { ok: false, reason: "localhost origins are not allowed in allowedCspOrigins" };
|
|
13349
|
+
}
|
|
13350
|
+
}
|
|
13351
|
+
const manifest = {
|
|
13352
|
+
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
13353
|
+
id: raw.id.trim(),
|
|
13354
|
+
title: raw.title.trim(),
|
|
13355
|
+
description: isNonEmptyString(raw.description) ? raw.description.trim() : "",
|
|
13356
|
+
collections,
|
|
13357
|
+
peerGroupId: isNonEmptyString(raw.peerGroupId) ? raw.peerGroupId.trim() : null,
|
|
13358
|
+
publisherId: isNonEmptyString(raw.publisherId) ? raw.publisherId.trim() : null,
|
|
13359
|
+
storedCommands: storedCommands.map((cmd) => ({
|
|
13360
|
+
id: cmd.id.trim(),
|
|
13361
|
+
collection: cmd.collection.trim(),
|
|
13362
|
+
kind: cmd.kind,
|
|
13363
|
+
allowedFields: Array.isArray(cmd.allowedFields) ? [...cmd.allowedFields] : null,
|
|
13364
|
+
requiresRole: isNonEmptyString(cmd.requiresRole) ? cmd.requiresRole.trim() : null
|
|
13365
|
+
})),
|
|
13366
|
+
allowedCspOrigins: allowedCspOrigins.map((o) => o.trim()),
|
|
13367
|
+
readOnly: storedCommands.length === 0
|
|
13368
|
+
};
|
|
13369
|
+
return { ok: true, manifest };
|
|
13370
|
+
}
|
|
13371
|
+
function collectionAllowed2(manifest, collectionName) {
|
|
13372
|
+
return manifest && Array.isArray(manifest.collections) && manifest.collections.includes(collectionName);
|
|
13373
|
+
}
|
|
13374
|
+
function getStoredCommand2(manifest, commandId) {
|
|
13375
|
+
if (!manifest || !Array.isArray(manifest.storedCommands)) {
|
|
13376
|
+
return null;
|
|
13377
|
+
}
|
|
13378
|
+
return manifest.storedCommands.find((cmd) => cmd.id === commandId) || null;
|
|
13379
|
+
}
|
|
13380
|
+
module2.exports = {
|
|
13381
|
+
MANIFEST_SCHEMA_VERSION,
|
|
13382
|
+
ID_PATTERN,
|
|
13383
|
+
validateDignityAppManifest: validateDignityAppManifest2,
|
|
13384
|
+
collectionAllowed: collectionAllowed2,
|
|
13385
|
+
getStoredCommand: getStoredCommand2
|
|
13386
|
+
};
|
|
13387
|
+
}
|
|
13388
|
+
});
|
|
13389
|
+
|
|
13273
13390
|
// src/index.js
|
|
13274
13391
|
var DignityP2P = require_dignity_p2p();
|
|
13275
13392
|
var createDefaultSignalingPool = require_create_default_signaling_pool();
|
|
@@ -13332,6 +13449,12 @@ var {
|
|
|
13332
13449
|
} = require_peer_group_tiers();
|
|
13333
13450
|
var { electBulkRelays, DEFAULT_BULK_RELAY_COUNT } = require_bulk_relay();
|
|
13334
13451
|
var DignityQueryReplica = require_query_replica();
|
|
13452
|
+
var {
|
|
13453
|
+
MANIFEST_SCHEMA_VERSION: DIGNITY_APP_MANIFEST_SCHEMA_VERSION,
|
|
13454
|
+
validateDignityAppManifest,
|
|
13455
|
+
collectionAllowed,
|
|
13456
|
+
getStoredCommand
|
|
13457
|
+
} = require_manifest();
|
|
13335
13458
|
module.exports = {
|
|
13336
13459
|
DignityP2P,
|
|
13337
13460
|
createDefaultSignalingPool,
|
|
@@ -13380,6 +13503,10 @@ module.exports = {
|
|
|
13380
13503
|
filterPeersByTier,
|
|
13381
13504
|
electBulkRelays,
|
|
13382
13505
|
DEFAULT_BULK_RELAY_COUNT,
|
|
13383
|
-
DignityQueryReplica
|
|
13506
|
+
DignityQueryReplica,
|
|
13507
|
+
DIGNITY_APP_MANIFEST_SCHEMA_VERSION,
|
|
13508
|
+
validateDignityAppManifest,
|
|
13509
|
+
collectionAllowed,
|
|
13510
|
+
getStoredCommand
|
|
13384
13511
|
};
|
|
13385
13512
|
//# sourceMappingURL=dignity.cjs.js.map
|