turtb 0.5.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/LICENSE +661 -0
- package/bin/turtb.js +259 -0
- package/lib/display/h.js +204 -0
- package/lib/display/helpers.js +78 -0
- package/lib/display/render.js +177 -0
- package/lib/display/shapes.js +91 -0
- package/lib/turtle/Signer.js +120 -0
- package/lib/turtle/Signer.test.js +38 -0
- package/lib/turtle/TurtleBranch.js +147 -0
- package/lib/turtle/TurtleBranch.test.js +89 -0
- package/lib/turtle/TurtleDictionary.js +157 -0
- package/lib/turtle/TurtleDictionary.test.js +331 -0
- package/lib/turtle/U8aTurtle.js +203 -0
- package/lib/turtle/U8aTurtle.test.js +60 -0
- package/lib/turtle/Workspace.js +62 -0
- package/lib/turtle/Workspace.test.js +63 -0
- package/lib/turtle/codecs/CodecType.js +36 -0
- package/lib/turtle/codecs/CodecTypeVersion.js +37 -0
- package/lib/turtle/codecs/Commit.js +10 -0
- package/lib/turtle/codecs/CompositeCodec.js +86 -0
- package/lib/turtle/codecs/TreeNode.js +38 -0
- package/lib/turtle/codecs/codec.js +441 -0
- package/lib/turtle/connections/AbstractUpdater.js +176 -0
- package/lib/turtle/connections/TurtleBranchMultiplexer.js +102 -0
- package/lib/turtle/connections/TurtleBranchMultiplexer.test.js +26 -0
- package/lib/turtle/connections/TurtleBranchUpdater.js +47 -0
- package/lib/turtle/connections/TurtleDB.js +165 -0
- package/lib/turtle/connections/TurtleDB.test.js +45 -0
- package/lib/turtle/connections/TurtleTalker.js +34 -0
- package/lib/turtle/connections/TurtleTalker.test.js +101 -0
- package/lib/turtle/utils.js +192 -0
- package/lib/turtle/utils.test.js +158 -0
- package/lib/utils/Assert.js +115 -0
- package/lib/utils/NestedSet.js +68 -0
- package/lib/utils/NestedSet.test.js +30 -0
- package/lib/utils/OWN_KEYS.js +1 -0
- package/lib/utils/Recaller.js +175 -0
- package/lib/utils/Recaller.test.js +75 -0
- package/lib/utils/TestRunner.js +200 -0
- package/lib/utils/TestRunner.test.js +144 -0
- package/lib/utils/TestRunnerConstants.js +13 -0
- package/lib/utils/combineUint8ArrayLikes.js +18 -0
- package/lib/utils/combineUint8Arrays.js +23 -0
- package/lib/utils/components.js +88 -0
- package/lib/utils/crypto.js +17 -0
- package/lib/utils/deepEqual.js +16 -0
- package/lib/utils/deepEqual.test.js +27 -0
- package/lib/utils/fileTransformer.js +16 -0
- package/lib/utils/handleRedirect.js +93 -0
- package/lib/utils/logger.js +24 -0
- package/lib/utils/nextTick.js +47 -0
- package/lib/utils/noble-secp256k1.js +602 -0
- package/lib/utils/proxyWithRecaller.js +51 -0
- package/lib/utils/toCombinedVersion.js +14 -0
- package/lib/utils/toSubVersions.js +14 -0
- package/lib/utils/toVersionCount.js +5 -0
- package/lib/utils/webSocketMuxFactory.js +123 -0
- package/lib/utils/zabacaba.js +25 -0
- package/package.json +24 -0
- package/src/ArchiveUpdater.js +99 -0
- package/src/S3Updater.js +99 -0
- package/src/archiveSync.js +28 -0
- package/src/fileSync.js +155 -0
- package/src/getExistenceLength.js +19 -0
- package/src/manageCert.js +36 -0
- package/src/originSync.js +75 -0
- package/src/outletSync.js +50 -0
- package/src/proxyFolder.js +195 -0
- package/src/s3Sync.js +32 -0
- package/src/webSync.js +101 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
function _constructValue (parts, node) {
|
|
2
|
+
if (parts == null) return null
|
|
3
|
+
if (typeof parts === 'function') {
|
|
4
|
+
parts = parts(node)
|
|
5
|
+
}
|
|
6
|
+
if (Array.isArray(parts)) {
|
|
7
|
+
const mappedParts = parts.map(part => {
|
|
8
|
+
if (typeof part === 'function') {
|
|
9
|
+
part = part(node)
|
|
10
|
+
}
|
|
11
|
+
if (part == null) return ''
|
|
12
|
+
if (part && part.type === 'part') {
|
|
13
|
+
return part.value
|
|
14
|
+
} else {
|
|
15
|
+
return part
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
if (mappedParts.length === 1) {
|
|
19
|
+
return mappedParts[0]
|
|
20
|
+
}
|
|
21
|
+
return mappedParts.join('')
|
|
22
|
+
}
|
|
23
|
+
return parts
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function _renderAttributes (attributes, node) {
|
|
27
|
+
let obj = {}
|
|
28
|
+
attributes.forEach(attribute => {
|
|
29
|
+
if (typeof attribute === 'function') {
|
|
30
|
+
attribute = attribute(node)
|
|
31
|
+
}
|
|
32
|
+
if (attribute == null) return
|
|
33
|
+
if (attribute && attribute.type === 'attribute') {
|
|
34
|
+
const name = attribute.name
|
|
35
|
+
if (name in obj) return
|
|
36
|
+
const value = _constructValue(attribute.value, node)
|
|
37
|
+
if (value == null) {
|
|
38
|
+
obj[name] = name
|
|
39
|
+
} else {
|
|
40
|
+
obj[name] = value
|
|
41
|
+
}
|
|
42
|
+
} else if (Array.isArray(attribute)) {
|
|
43
|
+
obj = Object.assign(_renderAttributes(attribute, node), obj)
|
|
44
|
+
} else if (typeof attribute === 'object') {
|
|
45
|
+
Object.entries(attribute).forEach(([name, value]) => {
|
|
46
|
+
if (name in obj) return
|
|
47
|
+
obj[name] = value
|
|
48
|
+
})
|
|
49
|
+
} else {
|
|
50
|
+
const name = attribute.toString()
|
|
51
|
+
if (name in obj) return
|
|
52
|
+
obj[name] = name
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
return obj
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function _setAttribute (element, name, value) {
|
|
59
|
+
if (element[name] !== value) {
|
|
60
|
+
try {
|
|
61
|
+
element[name] = value
|
|
62
|
+
} catch (e) {
|
|
63
|
+
// SVGs don't like getting their properties set and that's okay...
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!(typeof value).match(/(?:boolean|number|string)/)) {
|
|
67
|
+
value = name
|
|
68
|
+
}
|
|
69
|
+
const str = value.toString()
|
|
70
|
+
if (element.getAttribute(name) !== str) {
|
|
71
|
+
element.setAttribute(name, str)
|
|
72
|
+
}
|
|
73
|
+
return element
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function _setAttributes (element, attributes) {
|
|
77
|
+
Object.entries(attributes).forEach(([name, value]) => {
|
|
78
|
+
_setAttribute(element, name, value)
|
|
79
|
+
})
|
|
80
|
+
return element
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function _pruneAttributes (element, newAttributes, oldAttributes) {
|
|
84
|
+
const orphans = new Set(Object.keys(oldAttributes))
|
|
85
|
+
Object.keys(newAttributes).forEach(attribute => orphans.delete(attribute))
|
|
86
|
+
orphans.forEach(attribute => {
|
|
87
|
+
element.removeAttribute(attribute)
|
|
88
|
+
delete element[attribute]
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const _descriptionMap = new Map()
|
|
93
|
+
function _descriptionsToNodes (descriptions, xmlns, recaller, debugString) {
|
|
94
|
+
if (!Array.isArray(descriptions)) {
|
|
95
|
+
descriptions = [descriptions]
|
|
96
|
+
}
|
|
97
|
+
const nodes = []
|
|
98
|
+
descriptions.forEach(description => {
|
|
99
|
+
if (typeof description === 'function') {
|
|
100
|
+
description = description()
|
|
101
|
+
}
|
|
102
|
+
if (description != null) {
|
|
103
|
+
if (Array.isArray(description)) {
|
|
104
|
+
nodes.push(..._descriptionsToNodes(description, xmlns, recaller, debugString))
|
|
105
|
+
} else {
|
|
106
|
+
if (description.tag === null || description.tag === '') {
|
|
107
|
+
nodes.push(..._descriptionsToNodes(description.children, xmlns, recaller, debugString))
|
|
108
|
+
} else if (typeof description.tag === 'function') {
|
|
109
|
+
const attributes = _renderAttributes(description.attributes)
|
|
110
|
+
nodes.push(..._descriptionsToNodes(description.tag(attributes, description.children, description), attributes.xmlns || xmlns, recaller, debugString))
|
|
111
|
+
} else if (description.type) {
|
|
112
|
+
if (!_descriptionMap.has(description)) {
|
|
113
|
+
let node
|
|
114
|
+
if (description.type === 'textnode') {
|
|
115
|
+
node = document.createTextNode(description.value)
|
|
116
|
+
} else {
|
|
117
|
+
let oldAttributes = {}
|
|
118
|
+
let newAttributes = _renderAttributes(description.attributes, node)
|
|
119
|
+
node = document.createElementNS(newAttributes.xmlns || xmlns, description.tag, { is: newAttributes.is })
|
|
120
|
+
_setAttributes(node, newAttributes)
|
|
121
|
+
render(node, description.children, recaller, debugString, newAttributes.xmlns || xmlns)
|
|
122
|
+
recaller.watch(debugString, () => {
|
|
123
|
+
newAttributes = _renderAttributes(description.attributes, node)
|
|
124
|
+
_setAttributes(node, newAttributes)
|
|
125
|
+
_pruneAttributes(node, newAttributes, oldAttributes)
|
|
126
|
+
oldAttributes = newAttributes
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
_descriptionMap.set(description, node)
|
|
130
|
+
}
|
|
131
|
+
nodes.push(_descriptionMap.get(description))
|
|
132
|
+
} else {
|
|
133
|
+
nodes.push(document.createTextNode(description.toString()))
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
return nodes
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function _setChildren (element, descriptions, xmlns, recaller, debugString) {
|
|
142
|
+
const newNodes = _descriptionsToNodes(descriptions, xmlns, recaller, debugString)
|
|
143
|
+
newNodes.forEach((newNode, index) => {
|
|
144
|
+
while (element.childNodes[index] !== newNode) {
|
|
145
|
+
const oldNode = element.childNodes[index]
|
|
146
|
+
if (!oldNode) {
|
|
147
|
+
element.appendChild(newNode)
|
|
148
|
+
} else if (newNodes.indexOf(oldNode) > index) {
|
|
149
|
+
element.insertBefore(newNode, oldNode)
|
|
150
|
+
} else {
|
|
151
|
+
element.removeChild(oldNode)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
while (element.childNodes.length > newNodes.length) {
|
|
156
|
+
element.removeChild(element.lastChild)
|
|
157
|
+
}
|
|
158
|
+
return element
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @param {HTMLElement} element
|
|
163
|
+
* @param {array|object|function|null} descriptions
|
|
164
|
+
* @param {import('../utils/Recaller.js').Recaller} recaller
|
|
165
|
+
* @param {string} debugString
|
|
166
|
+
* @param {string} [xmlns='http://www.w3.org/1999/xhtml']
|
|
167
|
+
*/
|
|
168
|
+
export function render (element, descriptions, recaller, debugString, xmlns = 'http://www.w3.org/1999/xhtml') {
|
|
169
|
+
if (!descriptions) {
|
|
170
|
+
return _descriptionsToNodes(element, xmlns, recaller, debugString)
|
|
171
|
+
}
|
|
172
|
+
function f () {
|
|
173
|
+
return _setChildren(element, descriptions, xmlns, recaller, debugString)
|
|
174
|
+
}
|
|
175
|
+
recaller.watch(debugString, f)
|
|
176
|
+
return f
|
|
177
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { h } from './h.js'
|
|
2
|
+
|
|
3
|
+
export const cog = (attributes = {}, children = []) => {
|
|
4
|
+
attributes.viewBox ??= '-1 -1 2 2'
|
|
5
|
+
attributes.width ??= attributes.height ?? '1em'
|
|
6
|
+
attributes.height ??= attributes.width ?? '1em'
|
|
7
|
+
attributes.xmlns = 'http://www.w3.org/2000/svg'
|
|
8
|
+
const teeth = +attributes.teeth || 6
|
|
9
|
+
const holeR = +attributes.holeR || 0.25
|
|
10
|
+
const innerR = +attributes.innerR || 0.60
|
|
11
|
+
const outerR = +attributes.outerR || 0.90
|
|
12
|
+
const outerWeight = +attributes.outerWeight || 6
|
|
13
|
+
const innerWeight = +attributes.innerWeight || outerWeight * outerR / innerR
|
|
14
|
+
const transWeight = +attributes.transWeight || 3
|
|
15
|
+
const totalWeight = outerWeight + innerWeight + transWeight * 2
|
|
16
|
+
const toXY = (outerness, aroundness) => {
|
|
17
|
+
const r = outerR * outerness + innerR * (1 - outerness)
|
|
18
|
+
const theta = (aroundness - 0.25 - outerWeight / totalWeight / (2 * teeth)) * 2 * Math.PI
|
|
19
|
+
return `${Math.cos(theta) * r} ${Math.sin(theta) * r}`
|
|
20
|
+
}
|
|
21
|
+
return h`<svg ${attributes}>
|
|
22
|
+
<path d="
|
|
23
|
+
M ${toXY(1, 0)}
|
|
24
|
+
${[...Array(teeth).keys().map(tooth =>
|
|
25
|
+
`
|
|
26
|
+
A ${outerR} ${outerR} 0 0 1 ${toXY(1, (tooth + outerWeight / totalWeight) / teeth)}
|
|
27
|
+
L ${toXY(0, (tooth + (outerWeight + transWeight) / totalWeight) / teeth)}
|
|
28
|
+
A ${innerR} ${innerR} 0 0 1 ${toXY(0, (tooth + (outerWeight + transWeight + innerWeight) / totalWeight) / teeth)}
|
|
29
|
+
L ${toXY(1, (tooth + 1) / teeth)}
|
|
30
|
+
`
|
|
31
|
+
)].join('')}
|
|
32
|
+
Z
|
|
33
|
+
M 0 -${holeR}
|
|
34
|
+
A ${holeR} ${holeR} 0 1 0 0 ${holeR}
|
|
35
|
+
A ${holeR} ${holeR} 0 1 0 0 -${holeR}
|
|
36
|
+
Z
|
|
37
|
+
"/>
|
|
38
|
+
${children}
|
|
39
|
+
</svg>`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const star = (attributes = {}, children = []) => {
|
|
43
|
+
attributes.viewBox ??= '-1 -1 2 2'
|
|
44
|
+
attributes.width ??= attributes.height ?? '1em'
|
|
45
|
+
attributes.height ??= attributes.width ?? '1em'
|
|
46
|
+
attributes.xmlns = 'http://www.w3.org/2000/svg'
|
|
47
|
+
const points = +attributes.points || 5
|
|
48
|
+
const innerR = +attributes.innerR || 0.60
|
|
49
|
+
const outerR = +attributes.outerR || 0.90
|
|
50
|
+
const toXY = (outerness, aroundness) => {
|
|
51
|
+
const r = outerR * outerness + innerR * (1 - outerness)
|
|
52
|
+
const theta = (aroundness - 0.25) * 2 * Math.PI
|
|
53
|
+
return `${Math.cos(theta) * r} ${Math.sin(theta) * r}`
|
|
54
|
+
}
|
|
55
|
+
return h`<svg ${attributes}>
|
|
56
|
+
<path d="
|
|
57
|
+
M ${toXY(1, 0)}
|
|
58
|
+
${[...Array(points).keys().map(point =>
|
|
59
|
+
`
|
|
60
|
+
L ${toXY(0, (point + 0.5) / points)}
|
|
61
|
+
L ${toXY(1, (point + 1) / points)}
|
|
62
|
+
`
|
|
63
|
+
)].join('')}
|
|
64
|
+
Z
|
|
65
|
+
"/>
|
|
66
|
+
${children}
|
|
67
|
+
</svg>`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const equilateral = (attributes = {}, children = []) => {
|
|
71
|
+
attributes.viewBox ??= '-1 -1 2 2'
|
|
72
|
+
attributes.width ??= attributes.height ?? '1em'
|
|
73
|
+
attributes.height ??= attributes.width ?? '1em'
|
|
74
|
+
attributes.xmlns = 'http://www.w3.org/2000/svg'
|
|
75
|
+
const corners = +attributes.corners || 3
|
|
76
|
+
const r = +attributes.r || 0.90
|
|
77
|
+
const toXY = (aroundness) => {
|
|
78
|
+
const theta = (aroundness - 0.25) * 2 * Math.PI
|
|
79
|
+
return `${Math.cos(theta) * r} ${Math.sin(theta) * r}`
|
|
80
|
+
}
|
|
81
|
+
return h`<svg ${attributes}>
|
|
82
|
+
<path d="
|
|
83
|
+
M ${toXY(0)}
|
|
84
|
+
${[...Array(corners).keys().map(corner =>
|
|
85
|
+
`L ${toXY((corner + 1) / corners)}`
|
|
86
|
+
)].join('')}
|
|
87
|
+
Z
|
|
88
|
+
"/>
|
|
89
|
+
${children}
|
|
90
|
+
</svg>`
|
|
91
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { cryptoPromise, hashNameAndPassword } from '../utils/crypto.js'
|
|
2
|
+
import { getPublicKey, signAsync, verify } from '../utils/noble-secp256k1.js'
|
|
3
|
+
import { codec, COMMIT, splitEncodedCommit } from './codecs/codec.js'
|
|
4
|
+
import { AS_REFS } from './codecs/CodecType.js'
|
|
5
|
+
import { Commit } from './codecs/Commit.js'
|
|
6
|
+
import { b36ToUint8Array, uint8ArrayToB36 } from './utils.js'
|
|
7
|
+
import { combineUint8Arrays } from '../utils/combineUint8Arrays.js'
|
|
8
|
+
import { U8aTurtle } from './U8aTurtle.js'
|
|
9
|
+
import { logError } from '../utils/logger.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {import('./U8aTurtle.js').U8aTurtle} U8aTurtle
|
|
13
|
+
* @typedef {import('./codecs/Commit.js').Commit} Commit
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef KeyPair
|
|
18
|
+
* @property {string} privateKey
|
|
19
|
+
* @property {string} publicKey
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export class Signer {
|
|
23
|
+
/** @type {Object.<string, KeyPair>} */
|
|
24
|
+
keysByName = {}
|
|
25
|
+
|
|
26
|
+
constructor (username, password) {
|
|
27
|
+
this.username = username
|
|
28
|
+
this.hashwordPromise = hashNameAndPassword(username, password)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} turtlename
|
|
33
|
+
* @returns {KeyPair}
|
|
34
|
+
*/
|
|
35
|
+
async makeKeysFor (turtlename) {
|
|
36
|
+
if (!(this.keysByName[turtlename])) {
|
|
37
|
+
const hashword = await this.hashwordPromise
|
|
38
|
+
const privateKey = await hashNameAndPassword(turtlename, hashword)
|
|
39
|
+
const publicKey = uint8ArrayToB36(getPublicKey(privateKey))
|
|
40
|
+
this.keysByName[turtlename] = { privateKey, publicKey }
|
|
41
|
+
}
|
|
42
|
+
return this.keysByName[turtlename]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {string} turtleName
|
|
47
|
+
* @param {Uint8Array} uint8Array
|
|
48
|
+
* @returns {Uint8Array}
|
|
49
|
+
*/
|
|
50
|
+
async sign (turtleName, uint8Array) {
|
|
51
|
+
const { privateKey } = await this.makeKeysFor(turtleName)
|
|
52
|
+
const hash = await digestData(uint8Array)
|
|
53
|
+
const signature = await signAsync(hash, privateKey)
|
|
54
|
+
return signature.toCompactRawBytes()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {string} turtlename
|
|
59
|
+
* @param {number} commitAddress
|
|
60
|
+
* @param {U8aTurtle} u8aTurtle
|
|
61
|
+
* @param {U8aTurtle} committedTurtle
|
|
62
|
+
* @return {Uint8Array}
|
|
63
|
+
*/
|
|
64
|
+
async signCommit (turtlename, commitAddress, u8aTurtle, committedTurtle) {
|
|
65
|
+
const uint8Arrays = u8aTurtle.exportUint8Arrays((committedTurtle?.index ?? -1) + 1)
|
|
66
|
+
if (committedTurtle) {
|
|
67
|
+
const previousEncodedCommit = splitEncodedCommit(committedTurtle)[1]
|
|
68
|
+
uint8Arrays.unshift(previousEncodedCommit)
|
|
69
|
+
}
|
|
70
|
+
const signature = await this.sign(turtlename, combineUint8Arrays(uint8Arrays))
|
|
71
|
+
const commit = new Commit(commitAddress, signature)
|
|
72
|
+
return COMMIT.encode(commit, undefined, AS_REFS)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {U8aTurtle} u8aTurtle
|
|
78
|
+
* @param {string} publicKey
|
|
79
|
+
*/
|
|
80
|
+
export async function verifyTurtleCommit (u8aTurtle, publicKey) {
|
|
81
|
+
try {
|
|
82
|
+
const footer = u8aTurtle.getByte()
|
|
83
|
+
const codecVersion = codec.getCodecTypeVersion(footer)
|
|
84
|
+
if (codecVersion.codecType !== COMMIT) {
|
|
85
|
+
throw new Error('last value must be Commit')
|
|
86
|
+
}
|
|
87
|
+
/** @type {Commit} */
|
|
88
|
+
const commit = codecVersion.decode(u8aTurtle, undefined, AS_REFS)
|
|
89
|
+
let uint8Array = u8aTurtle.slice(undefined, -codecVersion.getWidth(u8aTurtle) - 1)
|
|
90
|
+
if (u8aTurtle.parent) {
|
|
91
|
+
const previousEncodedCommit = splitEncodedCommit(u8aTurtle.parent)[1]
|
|
92
|
+
uint8Array = combineUint8Arrays([previousEncodedCommit, uint8Array])
|
|
93
|
+
}
|
|
94
|
+
const hash = await digestData(uint8Array)
|
|
95
|
+
return verify(commit.signature, hash, b36ToUint8Array(publicKey))
|
|
96
|
+
} catch (error) {
|
|
97
|
+
logError(() => console.error(error))
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
*
|
|
104
|
+
* @param {string} publicKey
|
|
105
|
+
* @param {Uint8Array} uint8Array
|
|
106
|
+
* @param {Uint8Array} [previousUint8Array]
|
|
107
|
+
*/
|
|
108
|
+
export function verifyCommitU8a (publicKey, uint8Array, previousUint8Array) {
|
|
109
|
+
const u8aTurtle = new U8aTurtle(
|
|
110
|
+
uint8Array,
|
|
111
|
+
previousUint8Array && new U8aTurtle(previousUint8Array)
|
|
112
|
+
)
|
|
113
|
+
return verifyTurtleCommit(u8aTurtle, publicKey)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const digestData = async uint8Array => {
|
|
117
|
+
const { subtle } = await cryptoPromise
|
|
118
|
+
const digested = await subtle.digest('SHA-256', uint8Array)
|
|
119
|
+
return new Uint8Array(digested)
|
|
120
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { globalTestRunner, urlToName } from '../utils/TestRunner.js'
|
|
2
|
+
import { Signer, verifyCommitU8a, verifyTurtleCommit } from './Signer.js'
|
|
3
|
+
import { squashTurtle, U8aTurtle } from './U8aTurtle.js'
|
|
4
|
+
import { OPAQUE_UINT8ARRAY } from './codecs/codec.js'
|
|
5
|
+
|
|
6
|
+
globalTestRunner.describe(urlToName(import.meta.url), suite => {
|
|
7
|
+
suite.it('signs and verifies commits', async ({ assert }) => {
|
|
8
|
+
const signer = new Signer('signer1', 'password1')
|
|
9
|
+
const name = 'branch1'
|
|
10
|
+
const u8Array = new Uint8Array([1, 2, 3])
|
|
11
|
+
let u8aTurtle = new U8aTurtle(OPAQUE_UINT8ARRAY.encode(u8Array))
|
|
12
|
+
const u8aAddress = u8aTurtle.length - 1
|
|
13
|
+
const signedCommit = await signer.signCommit(name, u8aAddress, u8aTurtle)
|
|
14
|
+
|
|
15
|
+
u8aTurtle = new U8aTurtle(signedCommit, u8aTurtle)
|
|
16
|
+
u8aTurtle = squashTurtle(u8aTurtle, 0)
|
|
17
|
+
const committedTurtle = u8aTurtle
|
|
18
|
+
assert.equal(committedTurtle.lookup().document, u8Array)
|
|
19
|
+
const keys = await signer.makeKeysFor(name)
|
|
20
|
+
const verification0 = await verifyCommitU8a(keys.publicKey, u8aTurtle.getAncestorByIndex(0).uint8Array)
|
|
21
|
+
assert.assert(verification0)
|
|
22
|
+
const verification = await verifyTurtleCommit(committedTurtle, keys.publicKey)
|
|
23
|
+
assert.assert(verification)
|
|
24
|
+
|
|
25
|
+
const u8Array2 = new Uint8Array([4, 5, 6, 7])
|
|
26
|
+
u8aTurtle = new U8aTurtle(OPAQUE_UINT8ARRAY.encode(u8Array2), u8aTurtle)
|
|
27
|
+
const u8aAddress2 = u8aTurtle.length - 1
|
|
28
|
+
const signedCommit2 = await signer.signCommit(name, u8aAddress2, u8aTurtle, committedTurtle)
|
|
29
|
+
u8aTurtle = new U8aTurtle(signedCommit2, u8aTurtle)
|
|
30
|
+
u8aTurtle = squashTurtle(u8aTurtle, committedTurtle.index + 1)
|
|
31
|
+
assert.equal(u8aTurtle.lookup().document, u8Array2)
|
|
32
|
+
const verification2 = await verifyTurtleCommit(u8aTurtle, keys.publicKey)
|
|
33
|
+
assert.assert(verification2)
|
|
34
|
+
|
|
35
|
+
const verification3 = await verifyCommitU8a(keys.publicKey, u8aTurtle.uint8Array, u8aTurtle.parent.uint8Array)
|
|
36
|
+
assert.assert(verification3)
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Recaller } from '../utils/Recaller.js'
|
|
2
|
+
import { squashTurtle, U8aTurtle } from './U8aTurtle.js'
|
|
3
|
+
import { combineUint8Arrays } from '../utils/combineUint8Arrays.js'
|
|
4
|
+
import { combineUint8ArrayLikes } from '../utils/combineUint8ArrayLikes.js'
|
|
5
|
+
import { logInfo, logWarn } from '../utils/logger.js'
|
|
6
|
+
import { JSON_FILE, pathToType, TEXT_FILE } from '../utils/fileTransformer.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {import('./codecs/CodecType.js').CodecOptions} CodecOptions
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const _encodeUint8Array = uint8Array => {
|
|
13
|
+
const encodedLength = new Uint32Array([uint8Array.length])
|
|
14
|
+
return combineUint8ArrayLikes([encodedLength, uint8Array])
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class TurtleBranch {
|
|
18
|
+
/** @type {U8aTurtle} */
|
|
19
|
+
#u8aTurtle
|
|
20
|
+
#resolveNextUint8Array
|
|
21
|
+
#setNextUint8Array = uint8Array => {
|
|
22
|
+
const prevResolve = this.#resolveNextUint8Array
|
|
23
|
+
this.nextUint8Array = new Promise(resolve => { this.#resolveNextUint8Array = resolve })
|
|
24
|
+
prevResolve?.(uint8Array)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} name
|
|
29
|
+
* @param {Recaller} recaller
|
|
30
|
+
* @param {U8aTurtle} u8aTurtle
|
|
31
|
+
*/
|
|
32
|
+
constructor (name, recaller = new Recaller(name), u8aTurtle) {
|
|
33
|
+
if (!name) throw new Error('please name your branches')
|
|
34
|
+
this.name = name
|
|
35
|
+
this.recaller = recaller
|
|
36
|
+
this.#u8aTurtle = u8aTurtle
|
|
37
|
+
this.nextUint8Array = new Promise(resolve => { this.#resolveNextUint8Array = resolve })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @type {U8aTurtle} */
|
|
41
|
+
get u8aTurtle () {
|
|
42
|
+
this.recaller.reportKeyAccess(this, 'u8aTurtle', 'get', this.name)
|
|
43
|
+
return this.#u8aTurtle
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
set u8aTurtle (u8aTurtle) {
|
|
47
|
+
if (u8aTurtle === this.#u8aTurtle) return
|
|
48
|
+
if (u8aTurtle !== undefined) {
|
|
49
|
+
if (u8aTurtle.hasAncestor(this.#u8aTurtle)) {
|
|
50
|
+
const uint8Arrays = u8aTurtle.exportUint8Arrays((this.index ?? -1) + 1)
|
|
51
|
+
uint8Arrays.forEach(uint8Array => {
|
|
52
|
+
this.#setNextUint8Array(uint8Array)
|
|
53
|
+
})
|
|
54
|
+
} else {
|
|
55
|
+
logWarn(() => console.warn(`TurtleBranch, ${this.name}.u8aTurtle set to non-descendant (generators are broken now)`))
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
this.recaller.reportKeyMutation(this, 'u8aTurtle', 'set', this.name)
|
|
59
|
+
this.#u8aTurtle = u8aTurtle
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
append (uint8Array) {
|
|
63
|
+
if (!uint8Array?.length) {
|
|
64
|
+
throw new Error('bad Uint8Array')
|
|
65
|
+
}
|
|
66
|
+
this.u8aTurtle = new U8aTurtle(uint8Array, this.u8aTurtle)
|
|
67
|
+
return this.length - 1
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async * u8aTurtleGenerator () {
|
|
71
|
+
let lastIndex = -1
|
|
72
|
+
while (true) {
|
|
73
|
+
while (lastIndex < this.index) {
|
|
74
|
+
++lastIndex
|
|
75
|
+
yield this.u8aTurtle.getAncestorByIndex(lastIndex)
|
|
76
|
+
}
|
|
77
|
+
await this.nextUint8Array
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @returns {ReadableStream}
|
|
83
|
+
*/
|
|
84
|
+
makeReadableStream () {
|
|
85
|
+
let _controller
|
|
86
|
+
const turtleBranch = this
|
|
87
|
+
return new ReadableStream({
|
|
88
|
+
async start (controller) {
|
|
89
|
+
_controller = controller
|
|
90
|
+
for await (const u8aTurtle of turtleBranch.u8aTurtleGenerator()) {
|
|
91
|
+
_controller.enqueue(_encodeUint8Array(u8aTurtle.uint8Array))
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
cancel (reason) {
|
|
95
|
+
logInfo(() => console.log('stream cancelled', { reason }))
|
|
96
|
+
},
|
|
97
|
+
type: 'bytes'
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @returns {WritableStream}
|
|
103
|
+
*/
|
|
104
|
+
makeWritableStream () {
|
|
105
|
+
let u8aProgress = new Uint8Array()
|
|
106
|
+
let u8aLength
|
|
107
|
+
const turtleBranch = this
|
|
108
|
+
return new WritableStream({
|
|
109
|
+
write (chunk) {
|
|
110
|
+
u8aProgress = combineUint8Arrays([u8aProgress, chunk])
|
|
111
|
+
while (u8aProgress.length > 4 && u8aProgress.length > (u8aLength = new Uint32Array(u8aProgress.slice(0, 4).buffer)[0])) {
|
|
112
|
+
const uint8Array = u8aProgress.slice(4, u8aLength + 4)
|
|
113
|
+
turtleBranch.append(uint8Array)
|
|
114
|
+
u8aProgress = u8aProgress.slice(u8aLength + 4)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
get length () { return this.u8aTurtle?.length ?? 0 }
|
|
121
|
+
get index () { return this.u8aTurtle?.index ?? -1 }
|
|
122
|
+
getByte (address) { return this.u8aTurtle?.getAncestorByAddress?.(address)?.getByte?.(address) }
|
|
123
|
+
slice (start, end) { return this.u8aTurtle?.getAncestorByAddress?.(start)?.slice?.(start, end) }
|
|
124
|
+
squash (downToIndex) {
|
|
125
|
+
if (this.index === downToIndex) return
|
|
126
|
+
this.#u8aTurtle = squashTurtle(this.u8aTurtle, downToIndex)
|
|
127
|
+
this.recaller.reportKeyMutation(this, 'u8aTurtle', 'squash', this.name)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param {[optional_address:number, ...path:Array.<string>, optional_options:CodecOptions]} path
|
|
132
|
+
* @returns {any}
|
|
133
|
+
*/
|
|
134
|
+
lookup (...path) { return this.u8aTurtle?.lookup?.(...path) }
|
|
135
|
+
|
|
136
|
+
lookupFile (filename, asStored = false, address) {
|
|
137
|
+
const type = pathToType(filename)
|
|
138
|
+
const storedContent = address ? this.lookup(address) : this.lookup('document', 'value', filename)
|
|
139
|
+
if (asStored || !storedContent) return storedContent
|
|
140
|
+
if (storedContent?.symlink) return storedContent
|
|
141
|
+
if (storedContent instanceof Uint8Array) return storedContent
|
|
142
|
+
if (typeof storedContent === 'string') return storedContent
|
|
143
|
+
if (type === JSON_FILE) return JSON.stringify(storedContent, null, 2)
|
|
144
|
+
if (type === TEXT_FILE) return storedContent.join('\n')
|
|
145
|
+
return undefined
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { handleNextTick, tics } from '../utils/nextTick.js'
|
|
2
|
+
import { globalTestRunner, urlToName } from '../utils/TestRunner.js'
|
|
3
|
+
import { TurtleBranch } from './TurtleBranch.js'
|
|
4
|
+
|
|
5
|
+
globalTestRunner.describe(urlToName(import.meta.url), suite => {
|
|
6
|
+
suite.it('passes through U8aTurtle changes', ({ assert }) => {
|
|
7
|
+
const branch = new TurtleBranch('branch')
|
|
8
|
+
const indices = []
|
|
9
|
+
branch.recaller.watch('indices', () => {
|
|
10
|
+
indices.push(branch.index)
|
|
11
|
+
})
|
|
12
|
+
assert.equal(indices, [-1])
|
|
13
|
+
branch.append(new Uint8Array([0, 1, 2]))
|
|
14
|
+
handleNextTick()
|
|
15
|
+
assert.equal(indices, [-1, 0])
|
|
16
|
+
branch.append(new Uint8Array([3, 4, 5]))
|
|
17
|
+
handleNextTick()
|
|
18
|
+
assert.equal(indices, [-1, 0, 1])
|
|
19
|
+
branch.append(new Uint8Array([6, 7, 8]))
|
|
20
|
+
handleNextTick()
|
|
21
|
+
assert.equal(indices, [-1, 0, 1, 2])
|
|
22
|
+
branch.squash(1)
|
|
23
|
+
handleNextTick()
|
|
24
|
+
assert.equal(indices, [-1, 0, 1, 2, 1])
|
|
25
|
+
assert.equal(branch.u8aTurtle.uint8Array, new Uint8Array([3, 4, 5, 6, 7, 8]))
|
|
26
|
+
})
|
|
27
|
+
suite.it('streams append', async ({ assert }) => {
|
|
28
|
+
const primary = new TurtleBranch('primary')
|
|
29
|
+
const secondary = new TurtleBranch('secondary')
|
|
30
|
+
const readableStream = primary.makeReadableStream()
|
|
31
|
+
const writableStream = secondary.makeWritableStream()
|
|
32
|
+
readableStream.pipeTo(writableStream)
|
|
33
|
+
primary.append(new Uint8Array([1, 2, 3]))
|
|
34
|
+
await new Promise(resolve => setTimeout(resolve))
|
|
35
|
+
assert.equal(secondary.u8aTurtle.exportUint8Arrays(), [new Uint8Array([1, 2, 3])])
|
|
36
|
+
})
|
|
37
|
+
suite.it('streams set u8aTurtle', async ({ assert }) => {
|
|
38
|
+
const primary = new TurtleBranch('primary')
|
|
39
|
+
const secondary = new TurtleBranch('secondary')
|
|
40
|
+
const readableStream = primary.makeReadableStream()
|
|
41
|
+
const writableStream = secondary.makeWritableStream()
|
|
42
|
+
readableStream.pipeTo(writableStream)
|
|
43
|
+
primary.append(new Uint8Array([1, 2, 3]))
|
|
44
|
+
await new Promise(resolve => setTimeout(resolve))
|
|
45
|
+
assert.equal(secondary.u8aTurtle.exportUint8Arrays(), [new Uint8Array([1, 2, 3])])
|
|
46
|
+
|
|
47
|
+
const primaryBranched = new TurtleBranch('branched', undefined, primary.u8aTurtle)
|
|
48
|
+
primaryBranched.append(new Uint8Array([4, 5, 6]))
|
|
49
|
+
primaryBranched.append(new Uint8Array([7, 8, 9]))
|
|
50
|
+
primary.u8aTurtle = primaryBranched.u8aTurtle
|
|
51
|
+
await new Promise(resolve => setTimeout(resolve))
|
|
52
|
+
assert.equal(secondary.u8aTurtle.exportUint8Arrays(), [
|
|
53
|
+
new Uint8Array([1, 2, 3]),
|
|
54
|
+
new Uint8Array([4, 5, 6]),
|
|
55
|
+
new Uint8Array([7, 8, 9])
|
|
56
|
+
])
|
|
57
|
+
})
|
|
58
|
+
suite.it('streams established U8aTurtles', async ({ assert }) => {
|
|
59
|
+
const primary = new TurtleBranch('primary')
|
|
60
|
+
primary.append(new Uint8Array([1, 2, 3]))
|
|
61
|
+
primary.append(new Uint8Array([4, 5, 6]))
|
|
62
|
+
primary.append(new Uint8Array([7, 8, 9]))
|
|
63
|
+
const secondary = new TurtleBranch('secondary')
|
|
64
|
+
primary.makeReadableStream().pipeTo(secondary.makeWritableStream())
|
|
65
|
+
await tics(1)
|
|
66
|
+
assert.equal(secondary.u8aTurtle.exportUint8Arrays(), [
|
|
67
|
+
new Uint8Array([1, 2, 3]),
|
|
68
|
+
new Uint8Array([4, 5, 6]),
|
|
69
|
+
new Uint8Array([7, 8, 9])
|
|
70
|
+
])
|
|
71
|
+
})
|
|
72
|
+
suite.it('async generators', async ({ assert }) => {
|
|
73
|
+
const branch = new TurtleBranch('branch')
|
|
74
|
+
branch.append(new Uint8Array([1]))
|
|
75
|
+
branch.append(new Uint8Array([2]))
|
|
76
|
+
const uint8Arrays = []
|
|
77
|
+
;(async () => {
|
|
78
|
+
for await (const u8aTurtle of branch.u8aTurtleGenerator()) {
|
|
79
|
+
uint8Arrays.push(u8aTurtle.uint8Array)
|
|
80
|
+
}
|
|
81
|
+
})()
|
|
82
|
+
await tics(1)
|
|
83
|
+
assert.equal(uint8Arrays, [new Uint8Array([1]), new Uint8Array([2])])
|
|
84
|
+
branch.append(new Uint8Array([3]))
|
|
85
|
+
branch.append(new Uint8Array([4]))
|
|
86
|
+
await tics(1)
|
|
87
|
+
assert.equal(uint8Arrays, [new Uint8Array([1]), new Uint8Array([2]), new Uint8Array([3]), new Uint8Array([4])])
|
|
88
|
+
})
|
|
89
|
+
})
|