@cero-base/cero 1.15.1 → 1.16.0
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/package.json +2 -2
- package/src/build/index.js +1 -0
- package/src/build/schemas.js +8 -1
- package/src/handle/index.js +40 -5
- package/src/lib/operators.js +1 -1
- package/types/build/schemas.d.ts +2 -0
- package/types/handle/index.d.ts +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cero-base/cero",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "The ideal p2p API — everything is a handle, handles contain refs, refs contain rows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"test:node": "ls test/*.test.js | xargs -P1 -n1 brittle-node"
|
|
96
96
|
},
|
|
97
97
|
"dependencies": {
|
|
98
|
-
"@cero-base/core": "^1.
|
|
98
|
+
"@cero-base/core": "^1.16.0",
|
|
99
99
|
"b4a": "^1.8.1",
|
|
100
100
|
"bare-abort-controller": "^1.1.2",
|
|
101
101
|
"bare-crypto": "^1.15.3",
|
package/src/build/index.js
CHANGED
|
@@ -205,6 +205,7 @@ function register(name, node, ctx) {
|
|
|
205
205
|
schema: fqn,
|
|
206
206
|
fields: Object.keys(node.fields)
|
|
207
207
|
}
|
|
208
|
+
if (node.own) refEntry.own = true
|
|
208
209
|
const fileFields = fileFieldNames(node.fields)
|
|
209
210
|
if (fileFields.length) refEntry.files = fileFields
|
|
210
211
|
ctx.meta.refs[name] = refEntry
|
package/src/build/schemas.js
CHANGED
|
@@ -14,7 +14,14 @@ export const main = {
|
|
|
14
14
|
writer: required(bytes),
|
|
15
15
|
sig: required(bytes),
|
|
16
16
|
isIndexer: bool,
|
|
17
|
-
ts: int
|
|
17
|
+
ts: int,
|
|
18
|
+
// Who this writer belongs to, and at what rank. Admission carries its own
|
|
19
|
+
// authorization so add-writer can refuse exactly when the paired
|
|
20
|
+
// add-member would — the two apply independently, so without this a
|
|
21
|
+
// refused grant still leaves an admitted writer behind. Absent on a
|
|
22
|
+
// device you add for yourself (bootstrap), where there is nothing to cap.
|
|
23
|
+
memberId: string,
|
|
24
|
+
role: string
|
|
18
25
|
},
|
|
19
26
|
counter: {
|
|
20
27
|
name: required(string),
|
package/src/handle/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { Identity } from '@cero-base/core/identity'
|
|
|
12
12
|
import { Database } from '@cero-base/core/database'
|
|
13
13
|
import { epochEntries, blobEpochKey } from '@cero-base/core/database/encryption'
|
|
14
14
|
import { Pairing } from '@cero-base/core/pairing'
|
|
15
|
-
import { toId, grants, addWriterPayload } from '@cero-base/core/utils'
|
|
15
|
+
import { toId, grants, isRank, addWriterPayload } from '@cero-base/core/utils'
|
|
16
16
|
import { CeroError } from '@cero-base/core/errors'
|
|
17
17
|
import { Blobs } from '@cero-base/core/blobs'
|
|
18
18
|
import { decodeId } from '@cero-base/core/blobs/codec'
|
|
@@ -483,6 +483,10 @@ export class Handle extends ReadyResource {
|
|
|
483
483
|
throw CeroError.INVALID(
|
|
484
484
|
'invite() is not available on the root handle — open a child handle first'
|
|
485
485
|
)
|
|
486
|
+
// An invite is a capability: whoever holds it is admitted. Cap it here or
|
|
487
|
+
// a member mints one above their own rank, and the mismatch only surfaces
|
|
488
|
+
// at apply — as a silent drop that still leaves the joiner admitted.
|
|
489
|
+
if (opts?.role !== undefined && opts.role !== '') await this._checkGrant(opts.role)
|
|
486
490
|
const str = await this.pair.createInvite(opts)
|
|
487
491
|
const record = this.pair.recordOf(str)
|
|
488
492
|
if (record) {
|
|
@@ -548,6 +552,14 @@ export class Handle extends ReadyResource {
|
|
|
548
552
|
if (candidate.invite.role && !grants(candidate.invite.role, role)) {
|
|
549
553
|
throw CeroError.INVALID(`role '${role}' exceeds the invite role '${candidate.invite.role}'`)
|
|
550
554
|
}
|
|
555
|
+
// Refuse BEFORE confirm() — but without awaiting: confirm has to answer
|
|
556
|
+
// inside the pairing request's lifetime, and any read here drops it. Rank
|
|
557
|
+
// validation is synchronous and catches the dangerous case (an app role
|
|
558
|
+
// name silently grants nothing); the cap against our own rank is enforced
|
|
559
|
+
// at apply, where add-writer refuses to admit what it cannot grant.
|
|
560
|
+
if (!isRank(role)) {
|
|
561
|
+
throw CeroError.INVALID(`role '${role}' is not a rank (owner, admin, member, reader)`)
|
|
562
|
+
}
|
|
551
563
|
|
|
552
564
|
// confirm must answer within the incoming pairing request's lifetime —
|
|
553
565
|
// any await before it (even ~100ms) and the response is dropped, the
|
|
@@ -586,12 +598,32 @@ export class Handle extends ReadyResource {
|
|
|
586
598
|
sig,
|
|
587
599
|
master: this.identity.publicKey,
|
|
588
600
|
writer: writerKey,
|
|
601
|
+
// admission carries who and at what rank, so a grant we cannot make
|
|
602
|
+
// admits nobody — the two ops apply independently and cannot roll
|
|
603
|
+
// each other back
|
|
604
|
+
memberId: member.id,
|
|
605
|
+
role,
|
|
589
606
|
ts: member.updatedAt || Date.now()
|
|
590
607
|
})
|
|
591
608
|
await tx.call('add-member', member)
|
|
592
609
|
})
|
|
593
610
|
}
|
|
594
611
|
|
|
612
|
+
// Throws unless this handle may grant `role`: it must be a real rank, and
|
|
613
|
+
// never above our own. `grants` treats an unknown role as "no" everywhere,
|
|
614
|
+
// so an app role name must fail loudly here rather than silently downstream.
|
|
615
|
+
async _checkGrant(role) {
|
|
616
|
+
if (!isRank(role)) {
|
|
617
|
+
throw CeroError.INVALID(`role '${role}' is not a rank (owner, admin, member, reader)`)
|
|
618
|
+
}
|
|
619
|
+
const { data: me } = await this.store.get('members', this.identity.id)
|
|
620
|
+
// no member row yet = genesis (we are creating the room) — nothing to cap
|
|
621
|
+
if (!me) return
|
|
622
|
+
if (!grants(me.role, role)) {
|
|
623
|
+
throw CeroError.DENIED(null, `role '${role}' exceeds your own role '${me.role}'`)
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
595
627
|
/**
|
|
596
628
|
* Leave a child handle — removes it from the parent's `handles` collection
|
|
597
629
|
* and closes the session. No-op on root handles.
|
|
@@ -822,11 +854,11 @@ export class Handle extends ReadyResource {
|
|
|
822
854
|
* @param {string} id
|
|
823
855
|
* @returns {Promise<Handle>}
|
|
824
856
|
*/
|
|
825
|
-
async _load(type, id) {
|
|
857
|
+
async _load(type, id, opts) {
|
|
826
858
|
for (const c of this.children) if (c.id === id) return c
|
|
827
859
|
const existing = this._loading.get(id)
|
|
828
860
|
if (existing) return existing
|
|
829
|
-
const loading = this._reopen(type, id)
|
|
861
|
+
const loading = this._reopen(type, id, opts)
|
|
830
862
|
this._loading.set(id, loading)
|
|
831
863
|
try {
|
|
832
864
|
return await loading
|
|
@@ -843,7 +875,7 @@ export class Handle extends ReadyResource {
|
|
|
843
875
|
* @param {string} id
|
|
844
876
|
* @returns {Promise<Handle>}
|
|
845
877
|
*/
|
|
846
|
-
async _reopen(type, id) {
|
|
878
|
+
async _reopen(type, id, opts) {
|
|
847
879
|
const { data } = await this.store.get('handles', id)
|
|
848
880
|
if (!data) throw CeroError.UNKNOWN('handle', id)
|
|
849
881
|
if (data.type !== type)
|
|
@@ -868,7 +900,10 @@ export class Handle extends ReadyResource {
|
|
|
868
900
|
if (firstTime) {
|
|
869
901
|
await this._saveKeyPair(id, writer)
|
|
870
902
|
}
|
|
871
|
-
|
|
903
|
+
// a reopen must not silently re-arm auto-accept: `accept: false` is a
|
|
904
|
+
// host-approval gate, and one that comes back on the next open is worse
|
|
905
|
+
// than none at all
|
|
906
|
+
if (opts?.accept !== false) this._wireAccept(child, { role: opts?.role })
|
|
872
907
|
bind(child, type)
|
|
873
908
|
this.children.add(child)
|
|
874
909
|
this.emit('handle', child, {})
|
package/src/lib/operators.js
CHANGED
|
@@ -341,7 +341,7 @@ function snapshotStream(src, map) {
|
|
|
341
341
|
export const open = (ref, arg) => {
|
|
342
342
|
if (typeof arg === 'string') return ref.handle._join(arg, ref.name)
|
|
343
343
|
if (arg && typeof arg.invite === 'string') return ref.handle._join(arg.invite, ref.name)
|
|
344
|
-
if (arg && typeof arg.id === 'string') return ref.handle._load(ref.name, arg.id)
|
|
344
|
+
if (arg && typeof arg.id === 'string') return ref.handle._load(ref.name, arg.id, arg)
|
|
345
345
|
return ref.handle._create(ref.name, arg)
|
|
346
346
|
}
|
|
347
347
|
|
package/types/build/schemas.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export const main: {
|
|
|
8
8
|
sig: import("@cero-base/core").Prim;
|
|
9
9
|
isIndexer: import("@cero-base/core").Prim;
|
|
10
10
|
ts: import("@cero-base/core").Prim;
|
|
11
|
+
memberId: import("@cero-base/core").Prim;
|
|
12
|
+
role: import("@cero-base/core").Prim;
|
|
11
13
|
};
|
|
12
14
|
counter: {
|
|
13
15
|
name: import("@cero-base/core").Prim;
|
package/types/handle/index.d.ts
CHANGED
|
@@ -254,6 +254,7 @@ export class Handle extends ReadyResource {
|
|
|
254
254
|
* @returns {Promise<void>}
|
|
255
255
|
*/
|
|
256
256
|
accept(candidate: any, { role, name }?: AcceptOpts): Promise<void>;
|
|
257
|
+
_checkGrant(role: any): Promise<void>;
|
|
257
258
|
/**
|
|
258
259
|
* Leave a child handle — removes it from the parent's `handles` collection
|
|
259
260
|
* and closes the session. No-op on root handles.
|
|
@@ -299,7 +300,7 @@ export class Handle extends ReadyResource {
|
|
|
299
300
|
* @param {string} id
|
|
300
301
|
* @returns {Promise<Handle>}
|
|
301
302
|
*/
|
|
302
|
-
_load(type: string, id: string): Promise<Handle>;
|
|
303
|
+
_load(type: string, id: string, opts: any): Promise<Handle>;
|
|
303
304
|
/**
|
|
304
305
|
* Reconstruct a child handle by id. Reuses the stored writer keypair if
|
|
305
306
|
* available; otherwise generates a fresh one and claims writer capability.
|
|
@@ -308,7 +309,7 @@ export class Handle extends ReadyResource {
|
|
|
308
309
|
* @param {string} id
|
|
309
310
|
* @returns {Promise<Handle>}
|
|
310
311
|
*/
|
|
311
|
-
_reopen(type: string, id: string): Promise<Handle>;
|
|
312
|
+
_reopen(type: string, id: string, opts: any): Promise<Handle>;
|
|
312
313
|
/**
|
|
313
314
|
* Pause networking + storage. Idempotent; no-op on child handles.
|
|
314
315
|
*
|