dignity.js 0.8.1 → 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 CHANGED
@@ -519,6 +519,29 @@ npm run docs:dev
519
519
 
520
520
  If 4173 is busy, `docs:dev` auto-picks the next free port (4174, 4175, …) and prints the URLs.
521
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
+
522
545
  ## Docs and Examples
523
546
 
524
547
  - **Documentation:** [jose-compu.github.io/dignity.js](https://jose-compu.github.io/dignity.js/)
@@ -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