@cero-base/cero 0.8.9 → 1.0.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.
- package/README.md +88 -18
- package/package.json +20 -10
- package/src/build/builtins.js +10 -3
- package/src/build/index.js +24 -4
- package/src/build/schemas.js +20 -1
- package/src/handle/index.js +178 -69
- package/src/index.js +85 -64
- package/src/lib/constants.js +2 -1
- package/src/lib/operators.js +114 -53
- package/src/lib/peek.js +42 -0
- package/src/lib/utils.js +16 -3
- package/src/rpc/client.js +128 -14
- package/src/rpc/server.js +54 -7
- package/types/build/builtins.d.ts +5 -1
- package/types/build/index.d.ts +4 -21
- package/types/build/schemas.d.ts +19 -0
- package/types/handle/index.d.ts +49 -8
- package/types/index.d.ts +10 -5
- package/types/lib/constants.d.ts +1 -0
- package/types/lib/operators.d.ts +30 -10
- package/types/lib/peek.d.ts +10 -0
- package/types/lib/utils.d.ts +6 -3
- package/types/rpc/client.d.ts +3 -1
- package/types/rpc/server.d.ts +6 -5
package/README.md
CHANGED
|
@@ -56,6 +56,7 @@ Ships with TypeScript declarations (`.d.ts`) generated from JSDoc.
|
|
|
56
56
|
| `recoveryTimeout` | Bound on the recovery wait. |
|
|
57
57
|
| `routes` | Custom action handlers keyed by route name. |
|
|
58
58
|
| `encryptionKey` | Override the per-identity encryption key. |
|
|
59
|
+
| `onerror` | Called with background/async failures that would otherwise be swallowed (failed after-hooks, `onApply` callbacks, pairing candidate errors); the app decides how to log or report them. |
|
|
59
60
|
|
|
60
61
|
Reads `me.id`, `me.device`, `me.identity` for canonical metadata. `me.identity.toPhrase()` renders the seed phrase.
|
|
61
62
|
|
|
@@ -85,6 +86,15 @@ const recovered = await cero.restore(me, 'twelve words …')
|
|
|
85
86
|
| `{ … }` (plain object) | a child handle — its own scope, share by invite |
|
|
86
87
|
| `local: { … }` | device-only — never leaves this device |
|
|
87
88
|
|
|
89
|
+
A collection can declare **secondary indexes** for fast exact-field lookups:
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
cero.t.collection(
|
|
93
|
+
{ text: cero.t.string },
|
|
94
|
+
{ indexes: { 'by-text': ['text'] } } // name → field(s); query via `cero.get(ref, { text: '…' })`
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
88
98
|
## Operators
|
|
89
99
|
|
|
90
100
|
Every operator takes a `ref` (e.g. `me.profile`, `room.messages`) as the first arg.
|
|
@@ -95,8 +105,9 @@ Append a row to a collection. Generates an `id`, `createdAt`, and `updatedAt` if
|
|
|
95
105
|
|
|
96
106
|
```js
|
|
97
107
|
const { data } = await cero.put(room.messages, { text: 'hi' })
|
|
98
|
-
data.id // → 'abc…'
|
|
99
|
-
|
|
108
|
+
data.id // → 'abc…' (plus createdAt / updatedAt)
|
|
109
|
+
// `memberId` (the writer→member backlink) is stamped in the apply layer, so it
|
|
110
|
+
// surfaces on a later `cero.get`, not on the row this call returns.
|
|
100
111
|
```
|
|
101
112
|
|
|
102
113
|
### `cero.set(ref, row)`
|
|
@@ -110,19 +121,30 @@ await cero.set(room.messages, { id: 'abc', text: 'edited' }) // upsert by id
|
|
|
110
121
|
|
|
111
122
|
### `cero.get(ref, q?)`
|
|
112
123
|
|
|
113
|
-
Read.
|
|
124
|
+
Read. The shape of `q` decides what comes back:
|
|
114
125
|
|
|
115
126
|
```js
|
|
116
127
|
const { data } = await cero.get(me.profile) // single → the row (or null)
|
|
117
128
|
const { data } = await cero.get(room.messages, 'abc') // by id → the row (or null)
|
|
129
|
+
|
|
130
|
+
// collection → paginated, with range filters
|
|
118
131
|
const { data, total, size } = await cero.get(room.messages, {
|
|
119
|
-
gt: 'm-2025', //
|
|
132
|
+
gt: 'm-2025', // range: gt / gte / lt / lte
|
|
120
133
|
limit: 20,
|
|
121
134
|
reverse: true
|
|
122
|
-
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// search → case-insensitive substring across string fields
|
|
138
|
+
const { data } = await cero.get(room.messages, { search: 'jo sm' })
|
|
139
|
+
// every space-separated term must match (AND); folds case + diacritics ('jose' ⇢ 'José').
|
|
140
|
+
// `fields` restricts which fields are searched (default: all string fields):
|
|
141
|
+
const { data } = await cero.get(room.guests, { search: 'jose', fields: ['name'] })
|
|
142
|
+
|
|
143
|
+
// exact field match → routed through a declared secondary index when the field has one
|
|
144
|
+
const { data } = await cero.get(room.messages, { text: 'alpha' })
|
|
123
145
|
```
|
|
124
146
|
|
|
125
|
-
`total` is the unfiltered row count; `size` is what came back after `limit`.
|
|
147
|
+
`total` is the unfiltered row count; `size` is what came back after `limit`. `search` and exact-field filters compose with `limit` / `reverse`, and `cero.count(ref, q)` honors them too.
|
|
126
148
|
|
|
127
149
|
### `cero.del(ref, id)`
|
|
128
150
|
|
|
@@ -146,9 +168,9 @@ A Readable stream that re-emits the latest snapshot on every change. Always emit
|
|
|
146
168
|
|
|
147
169
|
```js
|
|
148
170
|
const stream = cero.watch(room.messages, { limit: 50, reverse: true })
|
|
149
|
-
|
|
171
|
+
for await (const { data, total, size } of stream) {
|
|
150
172
|
/* render */
|
|
151
|
-
}
|
|
173
|
+
}
|
|
152
174
|
// stops on room.close(), on stream.destroy(), or on `signal` abort:
|
|
153
175
|
const ac = new AbortController()
|
|
154
176
|
cero.watch(room.messages, null, { signal: ac.signal }) // ac.abort() ⇒ destroyed
|
|
@@ -191,6 +213,53 @@ cero.after(me.profile, ({ row }) => {
|
|
|
191
213
|
|
|
192
214
|
`ctx` is `{ op, name, row }` (plus `result` in `after`).
|
|
193
215
|
|
|
216
|
+
## Files
|
|
217
|
+
|
|
218
|
+
Store a file (avatar, image, attachment) and get a ready-to-render URL. Bytes live in a per-handle blob core — replicated to members on demand, never inlined into the log — so rows carry only a small self-describing id.
|
|
219
|
+
|
|
220
|
+
Every handle has a builtin `files` collection. `cero.put(handle.files, …)` uploads bytes (a `Buffer` or a `Readable`) and returns a file with a `.url`:
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
const { data: file } = await cero.put(me.files, {
|
|
224
|
+
data: bytes, // Buffer | Readable
|
|
225
|
+
name: 'cat.jpg',
|
|
226
|
+
type: 'image/jpeg'
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
img.src = file.url
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Read them back — every row carries a fresh `.url`:
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
const { data: files } = await cero.get(me.files) // list all
|
|
236
|
+
const { data: one } = await cero.get(me.files, file.id) // one, by id
|
|
237
|
+
cero.watch(me.files).on('data', ({ data }) => render(data)) // live
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### The `file()` column type
|
|
241
|
+
|
|
242
|
+
For a file referenced from a row — an avatar, a room icon, a message attachment — declare the field `cero.t.file()`. Store the file's `id`; read it back already resolved to `{ id, name, type, size, url }`, with **no extra lookup**:
|
|
243
|
+
|
|
244
|
+
```js
|
|
245
|
+
// schema.js
|
|
246
|
+
profile: cero.t.single({ name: cero.t.string, avatar: cero.t.file() })
|
|
247
|
+
|
|
248
|
+
// save: upload, then store the id on the row
|
|
249
|
+
const { data: pic } = await cero.put(me.files, { data: bytes, type: 'image/png' })
|
|
250
|
+
await cero.set(me.profile, { avatar: pic.id })
|
|
251
|
+
|
|
252
|
+
// read: the avatar comes back url-ready
|
|
253
|
+
const { data: profile } = await cero.get(me.profile)
|
|
254
|
+
img.src = profile.avatar.url
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`cero.t.file({ embed: true })` also stores the file's `name` inline — handy for a list of named attachments, so rendering needs no per-item lookup.
|
|
258
|
+
|
|
259
|
+
### URLs are local and ephemeral
|
|
260
|
+
|
|
261
|
+
A `.url` points at a localhost server this device runs, with a per-session token — it changes across restarts and is **not** shareable to other peers. Never persist a `.url`: store the **id** (cero does), and re-read to get a current one. Each member derives their own url from the same id, and the bytes download on demand when the url is first fetched.
|
|
262
|
+
|
|
194
263
|
## Custom operators
|
|
195
264
|
|
|
196
265
|
Your app's business logic lives as **custom operators** — plain functions whose first arg is the handle they act on, composing the built-ins. Because the built-ins resolve through the handle (a real DB on the core, an RPC proxy on the client), custom operators are symmetric over RPC for free.
|
|
@@ -340,8 +409,9 @@ The client side gets the **same operator API** as a local cero — `cero.put`, `
|
|
|
340
409
|
import { serve } from '@cero-base/cero/server'
|
|
341
410
|
import { spec } from './spec/index.js'
|
|
342
411
|
|
|
343
|
-
// serve builds and owns the root cero — pass storage + the built spec, not a handle
|
|
344
|
-
|
|
412
|
+
// serve builds and owns the root cero — pass storage + the built spec, not a handle.
|
|
413
|
+
// Returns a Server instance (call server.close() to shut down).
|
|
414
|
+
const server = await serve(Bare.IPC, { storage: './data', spec, seed: '…' })
|
|
345
415
|
```
|
|
346
416
|
|
|
347
417
|
### Client (the UI process)
|
|
@@ -366,14 +436,14 @@ The same `spec/index.js` is imported on both sides. `@cero-base/cero/build` emit
|
|
|
366
436
|
|
|
367
437
|
Everything the local API offers, plus lifecycle methods on returned handles:
|
|
368
438
|
|
|
369
|
-
| Operator / method
|
|
370
|
-
|
|
|
371
|
-
| `cero.put` / `set` / `get` / `del` / `count` / `watch` / `call`
|
|
372
|
-
| `cero.open(ref, …)` — create, join, load by id
|
|
373
|
-
| `room.invite({ role })` — mint an invite
|
|
374
|
-
| `room.revoke(invite)` — invalidate an outstanding invite; `true` if it existed
|
|
375
|
-
| `room.close()` — release the handle on the server
|
|
376
|
-
| `room.leave()` — remove yourself from the room and from your handles list
|
|
439
|
+
| Operator / method | Works over RPC |
|
|
440
|
+
| -------------------------------------------------------------------------------------------------------------------- | -------------- |
|
|
441
|
+
| `cero.put` / `set` / `get` / `del` / `count` / `watch` / `call` | ✓ |
|
|
442
|
+
| `cero.open(ref, …)` — create, join, load by id | ✓ |
|
|
443
|
+
| `room.invite({ role, expiresIn, multiUse })` — mint an invite (`multiUse: true` keeps it alive after the first join) | ✓ |
|
|
444
|
+
| `room.revoke(invite)` — invalidate an outstanding invite; `true` if it existed | ✓ |
|
|
445
|
+
| `room.close()` — release the handle on the server | ✓ |
|
|
446
|
+
| `room.leave()` — remove yourself from the room and from your handles list | ✓ |
|
|
377
447
|
|
|
378
448
|
`cero.watch(ref)` returns a Readable on both sides; snapshots flow as a server-streamed RPC.
|
|
379
449
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cero-base/cero",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "The ideal p2p API — everything is a handle, handles contain refs, refs contain rows.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/lekinox/cero-base.git",
|
|
13
|
+
"directory": "packages/cero"
|
|
14
|
+
},
|
|
6
15
|
"main": "./src/index.js",
|
|
7
16
|
"types": "./types/index.d.ts",
|
|
8
17
|
"files": [
|
|
@@ -39,7 +48,7 @@
|
|
|
39
48
|
"typesVersions": {
|
|
40
49
|
"*": {
|
|
41
50
|
"build": [
|
|
42
|
-
"types/
|
|
51
|
+
"types/build/index.d.ts"
|
|
43
52
|
],
|
|
44
53
|
"server": [
|
|
45
54
|
"types/rpc/server.d.ts"
|
|
@@ -64,10 +73,6 @@
|
|
|
64
73
|
"crypto": {
|
|
65
74
|
"bare": "bare-crypto",
|
|
66
75
|
"default": "crypto"
|
|
67
|
-
},
|
|
68
|
-
"abort-controller": {
|
|
69
|
-
"bare": "bare-abort-controller",
|
|
70
|
-
"default": "bare-abort-controller"
|
|
71
76
|
}
|
|
72
77
|
},
|
|
73
78
|
"scripts": {
|
|
@@ -78,25 +83,30 @@
|
|
|
78
83
|
"test": "ls test/*.test.js | xargs -P1 -n1 brittle-node"
|
|
79
84
|
},
|
|
80
85
|
"dependencies": {
|
|
81
|
-
"@cero-base/core": "^0.
|
|
86
|
+
"@cero-base/core": "^1.0.1",
|
|
82
87
|
"b4a": "^1.8.1",
|
|
83
88
|
"bare-abort-controller": "^1.1.2",
|
|
84
|
-
"bare-crypto": "^1.
|
|
89
|
+
"bare-crypto": "^1.15.3",
|
|
85
90
|
"bare-fs": "^4.7.2",
|
|
86
91
|
"bare-path": "^3.0.1",
|
|
87
92
|
"blind-pairing": "^2.3.1",
|
|
88
|
-
"compact-encoding": "^3.
|
|
93
|
+
"compact-encoding": "^3.2.0",
|
|
94
|
+
"corestore": "^7.10.1",
|
|
89
95
|
"hrpc": "^4.3.0",
|
|
96
|
+
"hypercore": "^11.33.1",
|
|
97
|
+
"hypercore-crypto": "^3.7.0",
|
|
98
|
+
"hypercore-storage": "^3.1.1",
|
|
90
99
|
"hyperdb": "^6.7.0",
|
|
91
100
|
"hyperdispatch": "^1.6.0",
|
|
92
101
|
"hyperschema": "^1.21.0",
|
|
93
102
|
"ready-resource": "^1.2.0",
|
|
94
103
|
"safety-catch": "^1.0.3",
|
|
104
|
+
"streamx": "^2.28.0",
|
|
95
105
|
"z32": "^1.1.0"
|
|
96
106
|
},
|
|
97
107
|
"devDependencies": {
|
|
98
108
|
"@hyperswarm/testnet": "^3.1.4",
|
|
99
|
-
"brittle": "^4.0.
|
|
109
|
+
"brittle": "^4.0.2",
|
|
100
110
|
"typescript": "^6.0.3"
|
|
101
111
|
},
|
|
102
112
|
"license": "Apache-2.0"
|
package/src/build/builtins.js
CHANGED
|
@@ -13,7 +13,8 @@ export const refs = {
|
|
|
13
13
|
members: { type: 'member' },
|
|
14
14
|
devices: { type: 'device' },
|
|
15
15
|
invites: { type: 'invite' },
|
|
16
|
-
handles: { type: 'handle' }
|
|
16
|
+
handles: { type: 'handle' },
|
|
17
|
+
files: { type: 'file' }
|
|
17
18
|
},
|
|
18
19
|
local: {
|
|
19
20
|
master: { type: 'master', kind: 'single' },
|
|
@@ -22,7 +23,7 @@ export const refs = {
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
export const
|
|
26
|
+
export const getHyperdbType = (prim) => DB_TYPE[prim] || 'string'
|
|
26
27
|
|
|
27
28
|
const at = (ns, n) => `@${ns}/${n}`
|
|
28
29
|
const keyOf = (def) => (def.kind === 'single' ? [] : ['id'])
|
|
@@ -30,7 +31,7 @@ const keyOf = (def) => (def.kind === 'single' ? [] : ['id'])
|
|
|
30
31
|
const fields = (map) =>
|
|
31
32
|
Object.entries(map).map(([name, m]) => ({
|
|
32
33
|
name,
|
|
33
|
-
type:
|
|
34
|
+
type: getHyperdbType(m.prim),
|
|
34
35
|
required: m.required === true
|
|
35
36
|
}))
|
|
36
37
|
|
|
@@ -102,7 +103,13 @@ export const rpcCommands = (ns) => {
|
|
|
102
103
|
request: { name: ref('req-restore') },
|
|
103
104
|
response: { name: ref('res-identity') }
|
|
104
105
|
},
|
|
106
|
+
{ name: 'seed', request: { name: ref('req-empty') }, response: { name: ref('res-seed') } },
|
|
105
107
|
{ name: 'add-row', request: { name: ref('req-row') }, response: { name: ref('res-data') } },
|
|
108
|
+
{
|
|
109
|
+
name: 'add-file',
|
|
110
|
+
request: { name: ref('req-add-file') },
|
|
111
|
+
response: { name: ref('res-data') }
|
|
112
|
+
},
|
|
106
113
|
{
|
|
107
114
|
name: 'add-handle',
|
|
108
115
|
request: { name: ref('req-row') },
|
package/src/build/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
builtinDispatches,
|
|
19
19
|
rpcTypes,
|
|
20
20
|
rpcCommands,
|
|
21
|
-
|
|
21
|
+
getHyperdbType
|
|
22
22
|
} from './builtins.js'
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -42,6 +42,8 @@ import {
|
|
|
42
42
|
* @param {BuildOpts} [opts]
|
|
43
43
|
* @returns {Promise<void>}
|
|
44
44
|
*/
|
|
45
|
+
export { getHyperdbType } from './builtins.js'
|
|
46
|
+
|
|
45
47
|
export async function build(specDir, schema, { ns = NS } = {}) {
|
|
46
48
|
const raw = /** @type {SchemaDefs & { local?: SchemaDefs }} */ (schema?.defs || schema)
|
|
47
49
|
if (!raw || typeof raw !== 'object') throw CeroError.REQUIRED('schema')
|
|
@@ -148,6 +150,12 @@ function compile(root, ns, scope = 'main') {
|
|
|
148
150
|
}
|
|
149
151
|
}
|
|
150
152
|
|
|
153
|
+
function fileFieldNames(fields) {
|
|
154
|
+
return Object.entries(fields)
|
|
155
|
+
.filter(([, m]) => m.prim === 'file')
|
|
156
|
+
.map(([name]) => name)
|
|
157
|
+
}
|
|
158
|
+
|
|
151
159
|
function register(name, node, ctx) {
|
|
152
160
|
const fqn = `@${ctx.ns}/${name}`
|
|
153
161
|
if (node.kind === 'action') {
|
|
@@ -160,7 +168,11 @@ function register(name, node, ctx) {
|
|
|
160
168
|
ctx.types.push({ name, compact: false, fields: fieldsFor(node.fields) })
|
|
161
169
|
ctx.collections.push({ name, schema: fqn, key: [] })
|
|
162
170
|
ctx.dispatches.push({ name: `set-${name}`, requestType: fqn })
|
|
163
|
-
ctx.
|
|
171
|
+
ctx.dispatches.push({ name: `del-${name}`, requestType: `@${ctx.ns}/del-by-id` })
|
|
172
|
+
const refEntry = { kind: 'single', path: [name], schema: fqn, fields: Object.keys(node.fields) }
|
|
173
|
+
const fileFields = fileFieldNames(node.fields)
|
|
174
|
+
if (fileFields.length) refEntry.files = fileFields
|
|
175
|
+
ctx.meta.refs[name] = refEntry
|
|
164
176
|
return
|
|
165
177
|
}
|
|
166
178
|
if (node.kind === 'collection') {
|
|
@@ -180,7 +192,15 @@ function register(name, node, ctx) {
|
|
|
180
192
|
ctx.dispatches.push({ name: `add-${name}`, requestType: fqn })
|
|
181
193
|
ctx.dispatches.push({ name: `set-${name}`, requestType: fqn })
|
|
182
194
|
ctx.dispatches.push({ name: `del-${name}`, requestType: `@${ctx.ns}/del-by-id` })
|
|
183
|
-
|
|
195
|
+
const refEntry = {
|
|
196
|
+
kind: 'collection',
|
|
197
|
+
path: [name],
|
|
198
|
+
schema: fqn,
|
|
199
|
+
fields: Object.keys(node.fields)
|
|
200
|
+
}
|
|
201
|
+
const fileFields = fileFieldNames(node.fields)
|
|
202
|
+
if (fileFields.length) refEntry.files = fileFields
|
|
203
|
+
ctx.meta.refs[name] = refEntry
|
|
184
204
|
if (node.indexes) {
|
|
185
205
|
for (const [idx, fields] of Object.entries(node.indexes)) {
|
|
186
206
|
ctx.indexes.push({ name: `${name}-${idx}`, collection: fqn, key: fields })
|
|
@@ -193,7 +213,7 @@ function register(name, node, ctx) {
|
|
|
193
213
|
function fieldsFor(fields) {
|
|
194
214
|
return Object.entries(fields).map(([name, marker]) => ({
|
|
195
215
|
name,
|
|
196
|
-
type:
|
|
216
|
+
type: getHyperdbType(marker.prim),
|
|
197
217
|
required: marker.required === true
|
|
198
218
|
}))
|
|
199
219
|
}
|
package/src/build/schemas.js
CHANGED
|
@@ -59,6 +59,14 @@ export const main = {
|
|
|
59
59
|
updatedAt: int,
|
|
60
60
|
index: uint
|
|
61
61
|
},
|
|
62
|
+
file: {
|
|
63
|
+
id: required(string),
|
|
64
|
+
memberId: string,
|
|
65
|
+
name: string,
|
|
66
|
+
createdAt: int,
|
|
67
|
+
updatedAt: int,
|
|
68
|
+
index: uint
|
|
69
|
+
},
|
|
62
70
|
claim: {
|
|
63
71
|
identity: required(bytes),
|
|
64
72
|
writer: required(bytes),
|
|
@@ -93,7 +101,8 @@ export const rpc = {
|
|
|
93
101
|
handle: required(string),
|
|
94
102
|
ref: required(string),
|
|
95
103
|
data: required(bytes),
|
|
96
|
-
local: bool
|
|
104
|
+
local: bool,
|
|
105
|
+
noUpsert: bool
|
|
97
106
|
},
|
|
98
107
|
'req-id': {
|
|
99
108
|
handle: required(string),
|
|
@@ -151,9 +160,19 @@ export const rpc = {
|
|
|
151
160
|
type: required(string),
|
|
152
161
|
name: string
|
|
153
162
|
},
|
|
163
|
+
'req-add-file': {
|
|
164
|
+
handle: required(string),
|
|
165
|
+
data: required(bytes),
|
|
166
|
+
name: string,
|
|
167
|
+
type: string
|
|
168
|
+
},
|
|
154
169
|
'res-identity': {
|
|
155
170
|
id: required(string),
|
|
156
171
|
deviceId: string,
|
|
172
|
+
fileBase: string,
|
|
173
|
+
fileToken: string
|
|
174
|
+
},
|
|
175
|
+
'res-seed': {
|
|
157
176
|
phrase: string
|
|
158
177
|
},
|
|
159
178
|
'res-ok': {
|