dignity.js 0.6.0 → 0.7.1

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.
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Live playground examples — browser-safe, in-memory mesh, PoW disabled for speed.
3
+ * User code receives: dignity (namespace), log (...args), helpers.
4
+ */
5
+ export const PLAYGROUND_DEMOS = [
6
+ {
7
+ id: 'two-peers-crud',
8
+ title: 'Two peers — create & read',
9
+ description: 'Wire alice and bob through InMemoryNetworkHub; replicate one object.',
10
+ code: `const { DignityP2P, InMemoryNetworkHub, InMemoryNetworkAdapter } = dignity;
11
+
12
+ const hub = new InMemoryNetworkHub();
13
+ const security = helpers.fastSecurity();
14
+
15
+ const alice = new DignityP2P({
16
+ nodeId: 'alice',
17
+ networkAdapter: new InMemoryNetworkAdapter(hub),
18
+ security
19
+ });
20
+ const bob = new DignityP2P({
21
+ nodeId: 'bob',
22
+ networkAdapter: new InMemoryNetworkAdapter(hub),
23
+ security
24
+ });
25
+
26
+ helpers.track(alice, bob);
27
+ await alice.start();
28
+ await bob.start();
29
+
30
+ await alice.create('notes', { title: 'hello decentralized world' }, { id: 'note-1' });
31
+ await helpers.sleep(30);
32
+
33
+ const onBob = bob.read('notes', 'note-1');
34
+ log('bob.read:', JSON.stringify(onBob?.data));
35
+
36
+ await alice.stop();
37
+ await bob.stop();`
38
+ },
39
+ {
40
+ id: 'room-discovery',
41
+ title: 'Room discovery — list peers',
42
+ description: 'joinDiscovery on a shared scope and list who is in the room.',
43
+ code: `const { DignityP2P, InMemoryNetworkHub, InMemoryNetworkAdapter } = dignity;
44
+
45
+ const hub = new InMemoryNetworkHub();
46
+ const security = helpers.fastSecurity();
47
+
48
+ const alice = new DignityP2P({ nodeId: 'alice', networkAdapter: new InMemoryNetworkAdapter(hub), security });
49
+ const bob = new DignityP2P({ nodeId: 'bob', networkAdapter: new InMemoryNetworkAdapter(hub), security });
50
+ const carol = new DignityP2P({ nodeId: 'carol', networkAdapter: new InMemoryNetworkAdapter(hub), security });
51
+
52
+ helpers.track(alice, bob, carol);
53
+ await alice.start();
54
+ await bob.start();
55
+ await carol.start();
56
+
57
+ await alice.joinDiscovery('lobby', { metadata: { nickname: 'Alice' } });
58
+ await bob.joinDiscovery('lobby', { metadata: { nickname: 'Bob' } });
59
+ await carol.joinDiscovery('lobby', { metadata: { nickname: 'Carol' } });
60
+ await helpers.sleep(40);
61
+
62
+ const peers = alice.listPeers('lobby', { includeSelf: false });
63
+ log('Peers in lobby:', peers.map((p) => p.peerId).join(', '));
64
+ log('Count:', peers.length);
65
+
66
+ await alice.leaveDiscovery('lobby');
67
+ await bob.leaveDiscovery('lobby');
68
+ await carol.leaveDiscovery('lobby');
69
+ await alice.stop();
70
+ await bob.stop();
71
+ await carol.stop();`
72
+ },
73
+ {
74
+ id: 'ownership',
75
+ title: 'Ownership — only owner updates',
76
+ description: 'Bob cannot update alice-owned record; alice can.',
77
+ code: `const { DignityP2P, InMemoryNetworkHub, InMemoryNetworkAdapter } = dignity;
78
+
79
+ const hub = new InMemoryNetworkHub();
80
+ const security = helpers.fastSecurity();
81
+
82
+ const alice = new DignityP2P({ nodeId: 'alice', networkAdapter: new InMemoryNetworkAdapter(hub), security });
83
+ const bob = new DignityP2P({ nodeId: 'bob', networkAdapter: new InMemoryNetworkAdapter(hub), security });
84
+ helpers.track(alice, bob);
85
+ await alice.start();
86
+ await bob.start();
87
+
88
+ await alice.create('games', { score: 0 }, { id: 'g1' });
89
+ await helpers.sleep(20);
90
+
91
+ let bobError = null;
92
+ try {
93
+ await bob.update('games', 'g1', { score: 99 });
94
+ } catch (err) {
95
+ bobError = err.message;
96
+ }
97
+ log('bob update rejected:', bobError || 'unexpected success');
98
+
99
+ await alice.update('games', 'g1', { score: 10 });
100
+ await helpers.sleep(20);
101
+ log('after alice update:', bob.read('games', 'g1')?.data);
102
+
103
+ await alice.stop();
104
+ await bob.stop();`
105
+ },
106
+ {
107
+ id: 'collaborators',
108
+ title: 'Collaborators — shared edit rights',
109
+ description: 'Owner grants bob collaborator access on one record.',
110
+ code: `const { DignityP2P, InMemoryNetworkHub, InMemoryNetworkAdapter } = dignity;
111
+
112
+ const hub = new InMemoryNetworkHub();
113
+ const security = helpers.fastSecurity();
114
+
115
+ const alice = new DignityP2P({ nodeId: 'alice', networkAdapter: new InMemoryNetworkAdapter(hub), security });
116
+ const bob = new DignityP2P({ nodeId: 'bob', networkAdapter: new InMemoryNetworkAdapter(hub), security });
117
+ helpers.track(alice, bob);
118
+ await alice.start();
119
+ await bob.start();
120
+
121
+ await alice.create('docs', { body: 'draft' }, { id: 'd1', collaborators: ['bob'] });
122
+ await helpers.sleep(20);
123
+
124
+ await bob.update('docs', 'd1', { body: 'edited by bob' });
125
+ await helpers.sleep(20);
126
+
127
+ log('alice sees:', alice.read('docs', 'd1')?.data);
128
+ log('bob sees:', bob.read('docs', 'd1')?.data);
129
+
130
+ await alice.stop();
131
+ await bob.stop();`
132
+ },
133
+ {
134
+ id: 'direct-message',
135
+ title: 'Direct messaging — encrypted DM',
136
+ description: 'Register peer keys and send a direct message.',
137
+ code: `const { DignityP2P, InMemoryNetworkHub, InMemoryNetworkAdapter } = dignity;
138
+
139
+ const hub = new InMemoryNetworkHub();
140
+ const security = helpers.fastSecurity({ signingEnabled: true, encryptionEnabled: true });
141
+
142
+ const alice = new DignityP2P({ nodeId: 'alice', networkAdapter: new InMemoryNetworkAdapter(hub), security });
143
+ const bob = new DignityP2P({ nodeId: 'bob', networkAdapter: new InMemoryNetworkAdapter(hub), security });
144
+ helpers.track(alice, bob);
145
+
146
+ let received = null;
147
+ bob.on('message', (event) => {
148
+ if (event.senderId === 'alice' && event.type === 'chat') {
149
+ received = event.payload;
150
+ }
151
+ });
152
+
153
+ await alice.start();
154
+ await bob.start();
155
+
156
+ alice.registerPeerPublicKey('bob', bob.getPublicKey());
157
+ bob.registerPeerPublicKey('alice', alice.getPublicKey());
158
+
159
+ await alice.sendDirectMessage('bob', 'chat', { text: 'private hello' });
160
+ await helpers.sleep(40);
161
+
162
+ log('bob received:', received);
163
+
164
+ await alice.stop();
165
+ await bob.stop();`
166
+ },
167
+ {
168
+ id: 'content-hash',
169
+ title: 'Content hash — record.hash',
170
+ description: 'Each record exposes a sha512 digest over canonical data.',
171
+ code: `const { DignityP2P, InMemoryNetworkHub, InMemoryNetworkAdapter } = dignity;
172
+
173
+ const hub = new InMemoryNetworkHub();
174
+ const security = helpers.fastSecurity();
175
+
176
+ const node = new DignityP2P({ nodeId: 'solo', networkAdapter: new InMemoryNetworkAdapter(hub), security });
177
+ helpers.track(node);
178
+ await node.start();
179
+
180
+ const created = await node.create('notes', { title: 'hash me' }, { id: 'n1' });
181
+ log('hash prefix:', created.hash?.slice(0, 12) + '...');
182
+ log('full hash:', created.hash);
183
+
184
+ const updated = await node.update('notes', 'n1', { title: 'hash me v2' });
185
+ log('hash changed:', updated.hash !== created.hash);
186
+
187
+ await node.stop();`
188
+ },
189
+ {
190
+ id: 'concurrency',
191
+ title: 'Concurrency — expectedVersion conflict',
192
+ description: 'Stale baseVersion is rejected; listen for conflict events.',
193
+ code: `const { DignityP2P, InMemoryNetworkHub, InMemoryNetworkAdapter } = dignity;
194
+
195
+ const hub = new InMemoryNetworkHub();
196
+ const security = helpers.fastSecurity();
197
+
198
+ const alice = new DignityP2P({ nodeId: 'alice', networkAdapter: new InMemoryNetworkAdapter(hub), security });
199
+ const bob = new DignityP2P({ nodeId: 'bob', networkAdapter: new InMemoryNetworkAdapter(hub), security });
200
+ helpers.track(alice, bob);
201
+
202
+ const conflicts = [];
203
+ bob.on('conflict', (event) => conflicts.push(event));
204
+
205
+ await alice.start();
206
+ await bob.start();
207
+
208
+ const record = await alice.create('scores', { points: 0 }, { id: 's1', collaborators: ['bob'] });
209
+ await helpers.sleep(20);
210
+
211
+ await bob.update('scores', 's1', { points: 5 });
212
+ await helpers.sleep(20);
213
+
214
+ let staleError = null;
215
+ try {
216
+ await alice.update('scores', 's1', { points: 1 }, { expectedVersion: record.version });
217
+ } catch (err) {
218
+ staleError = err.message;
219
+ }
220
+
221
+ log('stale write error:', staleError || 'none');
222
+ log('conflict events on bob:', conflicts.length);
223
+ log('final score:', bob.read('scores', 's1')?.data);
224
+
225
+ await alice.stop();
226
+ await bob.stop();`
227
+ },
228
+ {
229
+ id: 'scoped-passwords',
230
+ title: 'Scoped broadcast passwords',
231
+ description: 'Different broadcastScope values use different team passwords.',
232
+ code: `const { DignityP2P, InMemoryNetworkHub, InMemoryNetworkAdapter } = dignity;
233
+
234
+ const hub = new InMemoryNetworkHub();
235
+
236
+ const red = new DignityP2P({
237
+ nodeId: 'red-1',
238
+ networkAdapter: new InMemoryNetworkAdapter(hub),
239
+ security: helpers.fastSecurity({
240
+ appPassword: 'fallback',
241
+ broadcastPasswords: { 'team:red': 'red-secret', 'team:blue': 'blue-secret' }
242
+ })
243
+ });
244
+ const blue = new DignityP2P({
245
+ nodeId: 'blue-1',
246
+ networkAdapter: new InMemoryNetworkAdapter(hub),
247
+ security: helpers.fastSecurity({
248
+ appPassword: 'fallback',
249
+ broadcastPasswords: { 'team:red': 'red-secret', 'team:blue': 'blue-secret' }
250
+ })
251
+ });
252
+ helpers.track(red, blue);
253
+ await red.start();
254
+ await blue.start();
255
+
256
+ await red.create('flags', { team: 'red' }, { id: 'f1', broadcastScope: 'team:red' });
257
+ await blue.create('flags', { team: 'blue' }, { id: 'f2', broadcastScope: 'team:blue' });
258
+ await helpers.sleep(40);
259
+
260
+ log('red sees own:', !!red.read('flags', 'f1'));
261
+ log('red sees blue flag:', !!red.read('flags', 'f2'));
262
+ log('blue sees red flag:', !!blue.read('flags', 'f1'));
263
+
264
+ await red.stop();
265
+ await blue.stop();`
266
+ },
267
+ {
268
+ id: 'credential-keys',
269
+ title: 'Credential-derived keys',
270
+ description: 'Derive the same signing/encryption keys from username + password.',
271
+ code: `const { deriveKeyPairFromCredentials, keyPairToPublicBundle } = dignity;
272
+
273
+ const opts = { username: 'alice', password: 'user-secret', kdfIterations: 1000 };
274
+
275
+ const first = await deriveKeyPairFromCredentials(opts);
276
+ const second = await deriveKeyPairFromCredentials(opts);
277
+
278
+ const a = keyPairToPublicBundle(first);
279
+ const b = keyPairToPublicBundle(second);
280
+
281
+ log('same signing key:', a.signingPublicKey === b.signingPublicKey);
282
+ log('same encryption key:', a.encryptionPublicKey === b.encryptionPublicKey);
283
+ log('generation:', first.generation);
284
+
285
+ const gen2 = await deriveKeyPairFromCredentials({ ...opts, generation: 2 });
286
+ log('gen2 differs:', keyPairToPublicBundle(gen2).signingPublicKey !== a.signingPublicKey);`
287
+ },
288
+ {
289
+ id: 'identity-rotation',
290
+ title: 'Identity rotation — peer adopts new keys',
291
+ description: 'Rotate generation, adopt keys locally, broadcast to a peer.',
292
+ code: `const {
293
+ DignityP2P,
294
+ InMemoryNetworkHub,
295
+ InMemoryNetworkAdapter,
296
+ deriveKeyPairFromCredentials,
297
+ keyPairToPublicBundle,
298
+ revokeAndRotateIdentity
299
+ } = dignity;
300
+
301
+ const hub = new InMemoryNetworkHub();
302
+ const security = helpers.fastSecurity({ signingEnabled: true, encryptionEnabled: true });
303
+
304
+ const alice = new DignityP2P({ nodeId: 'alice', networkAdapter: new InMemoryNetworkAdapter(hub), security });
305
+ const bob = new DignityP2P({ nodeId: 'bob', networkAdapter: new InMemoryNetworkAdapter(hub), security });
306
+ helpers.track(alice, bob);
307
+
308
+ const gen1 = await deriveKeyPairFromCredentials({
309
+ username: 'alice',
310
+ password: 'secret',
311
+ generation: 1,
312
+ kdfIterations: 1000
313
+ });
314
+
315
+ const rotationResult = await revokeAndRotateIdentity({
316
+ username: 'alice',
317
+ password: 'secret',
318
+ currentGeneration: 1,
319
+ kdfIterations: 1000,
320
+ reason: 'demo rotation'
321
+ });
322
+
323
+ await alice.start();
324
+ await bob.start();
325
+
326
+ await alice.adoptDerivedIdentityKeyPair(gen1, { generation: 1 });
327
+ bob.registerPeerPublicKey('alice', keyPairToPublicBundle(gen1), { generation: 1 });
328
+ log('bob gen before:', bob.getPeerIdentityGeneration('alice'));
329
+
330
+ // Broadcast while alice still signs with gen-1 keys; then adopt gen-2 locally.
331
+ await alice.broadcastIdentityRotation(rotationResult.rotation, { broadcastScope: 'identity:alice' });
332
+ await alice.adoptDerivedIdentityKeyPair(rotationResult.nextKeyPair, { generation: 2 });
333
+ await helpers.sleep(50);
334
+
335
+ log('bob gen after:', bob.getPeerIdentityGeneration('alice'));
336
+ log('keys upgraded:', bob.getPeerIdentityState('alice')?.publicKey?.signingPublicKey
337
+ === keyPairToPublicBundle(rotationResult.nextKeyPair).signingPublicKey);
338
+
339
+ await alice.stop();
340
+ await bob.stop();`
341
+ }
342
+ ];
@@ -0,0 +1,277 @@
1
+ .playground-page {
2
+ --playground-split: minmax(0, 1fr) minmax(0, 1fr);
3
+ display: flex;
4
+ flex-direction: column;
5
+ min-height: calc(100vh - var(--header-height));
6
+ }
7
+
8
+ .playground-intro {
9
+ padding: 20px 24px 0;
10
+ max-width: 1200px;
11
+ }
12
+
13
+ .playground-intro h1 {
14
+ margin: 0 0 8px;
15
+ font-size: 1.75rem;
16
+ color: var(--heading);
17
+ }
18
+
19
+ .playground-intro p {
20
+ margin: 0 0 16px;
21
+ color: var(--text-muted);
22
+ max-width: 72ch;
23
+ }
24
+
25
+ .playground-controls {
26
+ display: flex;
27
+ flex-wrap: wrap;
28
+ align-items: flex-end;
29
+ gap: 12px 20px;
30
+ padding: 0 24px 16px;
31
+ max-width: 1200px;
32
+ }
33
+
34
+ .playground-controls label {
35
+ display: flex;
36
+ flex-direction: column;
37
+ gap: 6px;
38
+ font-size: 0.8125rem;
39
+ font-weight: 600;
40
+ color: var(--text-muted);
41
+ min-width: 280px;
42
+ flex: 1;
43
+ }
44
+
45
+ .playground-controls select {
46
+ font: inherit;
47
+ font-weight: 500;
48
+ color: var(--text);
49
+ padding: 8px 12px;
50
+ border: 1px solid var(--border);
51
+ border-radius: var(--radius);
52
+ background: var(--bg);
53
+ }
54
+
55
+ .playground-controls__hint {
56
+ flex: 1 1 100%;
57
+ margin: 0;
58
+ font-size: 0.875rem;
59
+ color: var(--text-muted);
60
+ }
61
+
62
+ .playground-split {
63
+ flex: 1;
64
+ display: grid;
65
+ grid-template-columns: var(--playground-split);
66
+ gap: 0;
67
+ min-height: 0;
68
+ border-top: 1px solid var(--border);
69
+ }
70
+
71
+ .playground-pane {
72
+ display: flex;
73
+ flex-direction: column;
74
+ min-height: 0;
75
+ min-width: 0;
76
+ }
77
+
78
+ .playground-pane + .playground-pane {
79
+ border-left: 1px solid var(--border);
80
+ }
81
+
82
+ .playground-pane__head {
83
+ display: flex;
84
+ align-items: center;
85
+ justify-content: space-between;
86
+ gap: 12px;
87
+ padding: 10px 16px;
88
+ background: var(--bg-subtle);
89
+ border-bottom: 1px solid var(--border);
90
+ font-size: 0.8125rem;
91
+ font-weight: 600;
92
+ color: var(--text-muted);
93
+ text-transform: uppercase;
94
+ letter-spacing: 0.04em;
95
+ }
96
+
97
+ .playground-pane__actions {
98
+ display: flex;
99
+ gap: 8px;
100
+ }
101
+
102
+ .playground-btn {
103
+ font: inherit;
104
+ font-size: 0.8125rem;
105
+ font-weight: 600;
106
+ text-transform: none;
107
+ letter-spacing: 0;
108
+ padding: 6px 12px;
109
+ border-radius: var(--radius);
110
+ border: 1px solid var(--border);
111
+ background: var(--bg);
112
+ color: var(--text);
113
+ cursor: pointer;
114
+ }
115
+
116
+ .playground-btn:hover {
117
+ border-color: var(--accent);
118
+ color: var(--accent);
119
+ }
120
+
121
+ .playground-btn--primary {
122
+ background: var(--accent);
123
+ border-color: var(--accent);
124
+ color: #fff;
125
+ }
126
+
127
+ .playground-btn--primary:hover {
128
+ background: var(--accent-hover);
129
+ border-color: var(--accent-hover);
130
+ color: #fff;
131
+ }
132
+
133
+ .playground-btn:disabled {
134
+ opacity: 0.55;
135
+ cursor: not-allowed;
136
+ }
137
+
138
+ .playground-toolbar__status {
139
+ font-size: 0.75rem;
140
+ font-weight: 500;
141
+ text-transform: none;
142
+ letter-spacing: 0;
143
+ color: var(--text-muted);
144
+ }
145
+
146
+ .playground-toolbar__status.is-running {
147
+ color: var(--accent);
148
+ }
149
+
150
+ .playground-toolbar__status.is-ok {
151
+ color: var(--green);
152
+ }
153
+
154
+ .playground-toolbar__status.is-error {
155
+ color: #cf222e;
156
+ }
157
+
158
+ .playground-editor-wrap {
159
+ position: relative;
160
+ flex: 1;
161
+ min-height: 360px;
162
+ overflow: hidden;
163
+ display: grid;
164
+ background: var(--bg);
165
+ }
166
+
167
+ .playground-editor-highlight,
168
+ #code-editor {
169
+ grid-area: 1 / 1;
170
+ width: 100%;
171
+ height: 100%;
172
+ min-height: 360px;
173
+ margin: 0;
174
+ padding: 16px;
175
+ border: 0;
176
+ font-family: var(--font-mono);
177
+ font-size: 0.875rem;
178
+ line-height: 1.55;
179
+ tab-size: 2;
180
+ overflow: auto;
181
+ white-space: pre;
182
+ word-wrap: normal;
183
+ overflow-wrap: normal;
184
+ box-sizing: border-box;
185
+ }
186
+
187
+ .playground-editor-highlight {
188
+ pointer-events: none;
189
+ z-index: 0;
190
+ background: var(--bg);
191
+ color: var(--text);
192
+ }
193
+
194
+ .playground-editor-highlight code {
195
+ display: block;
196
+ min-height: calc(100% - 32px);
197
+ padding: 0;
198
+ margin: 0;
199
+ font: inherit;
200
+ line-height: inherit;
201
+ background: transparent !important;
202
+ border: 0 !important;
203
+ border-radius: 0 !important;
204
+ color: inherit;
205
+ }
206
+
207
+ .playground-editor-highlight code.hljs {
208
+ padding: 0 !important;
209
+ border: 0 !important;
210
+ border-left: 0 !important;
211
+ }
212
+
213
+ #code-editor {
214
+ resize: none;
215
+ z-index: 1;
216
+ color: transparent !important;
217
+ -webkit-text-fill-color: transparent !important;
218
+ caret-color: var(--text);
219
+ background: transparent;
220
+ }
221
+
222
+ #code-editor:focus {
223
+ outline: none;
224
+ box-shadow: inset 0 0 0 2px var(--accent-soft);
225
+ }
226
+
227
+ #code-editor::selection {
228
+ background: rgba(91, 127, 255, 0.35);
229
+ color: transparent;
230
+ -webkit-text-fill-color: transparent;
231
+ }
232
+
233
+ @media (prefers-color-scheme: dark) {
234
+ .playground-editor-wrap,
235
+ .playground-editor-highlight {
236
+ background: #0d1117;
237
+ color: #e6edf3;
238
+ }
239
+
240
+ #code-editor {
241
+ caret-color: #e6edf3;
242
+ }
243
+ }
244
+
245
+ #output {
246
+ flex: 1;
247
+ min-height: 360px;
248
+ margin: 0;
249
+ padding: 16px;
250
+ overflow: auto;
251
+ font-family: var(--font-mono);
252
+ font-size: 0.875rem;
253
+ line-height: 1.55;
254
+ background: #0d1117;
255
+ color: #e6edf3;
256
+ }
257
+
258
+ .playground-output__line {
259
+ white-space: pre-wrap;
260
+ word-break: break-word;
261
+ padding: 2px 0;
262
+ }
263
+
264
+ .playground-output__line.is-error {
265
+ color: #ff7b72;
266
+ }
267
+
268
+ @media (max-width: 960px) {
269
+ .playground-split {
270
+ grid-template-columns: 1fr;
271
+ }
272
+
273
+ .playground-pane + .playground-pane {
274
+ border-left: 0;
275
+ border-top: 1px solid var(--border);
276
+ }
277
+ }