-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathbin.js
executable file
·277 lines (257 loc) · 11.8 KB
/
bin.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
#!/usr/bin/env node
/* global WritableStream */
import sade from 'sade'
import dotenv from 'dotenv'
import fs from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { inspect } from 'node:util'
import { Writable } from 'node:stream'
import * as Link from 'multiformats/link'
import * as ed25519 from '@ucanto/principal/ed25519'
import { DID, UCAN } from '@ucanto/core'
import * as Delegation from '@ucanto/core/delegation'
import { CAR, HTTP } from '@ucanto/transport'
import * as Client from '@web3-storage/content-claims/client'
import { Assert } from '@web3-storage/content-claims/capability'
import { CARReaderStream } from 'carstream'
import duration from 'parse-duration'
const __dirname = dirname(fileURLToPath(import.meta.url))
dotenv.config({ path: join(__dirname, '.env') })
const serviceURL = new URL(process.env.SERVICE_URL ?? 'https://claims.web3.storage')
const servicePrincipal = DID.parse(process.env.SERVICE_DID ?? 'did:web:claims.web3.storage')
if (servicePrincipal.did() !== 'did:web:claims.web3.storage') {
console.warn(`WARN: using ${servicePrincipal.did()}`)
}
const connection = Client.connect({
id: servicePrincipal,
codec: CAR.outbound,
channel: HTTP.open({
url: serviceURL,
method: 'POST'
})
})
const prog = sade('claim').version('0.0.0')
prog
.command('location <content> <url>')
.describe('Generate a location claim that asserts the content (CAR CID) can be found at the URLs.')
.option('-o, --output', 'Write output to this file.')
.option('-e, --expire', 'Duration after which claim expires e.g \'1min\' or \'1hr\'', '30s')
.example('location bagbaierae3n6cey3feykv3h5imue3eustl656dajifuddj3zedhpdofje3za https://s3.url/bagbaierae3n6cey3feykv3h5imue3eustl656dajifuddj3zedhpdofje3za.car -o location.claim')
.action(async (contentArg, urlArg, opts) => {
const content = Link.parse(contentArg)
const urls = [urlArg, ...opts._]
const signer = getSigner()
const invocation = Assert.location.invoke({
issuer: signer,
audience: servicePrincipal,
with: signer.did(),
nb: { content, location: urls },
expiration: toUcanExpiration(opts.expire)
})
await archiveClaim(invocation, opts.output)
})
.command('partition <content> <part>')
.describe('Generate a partition claim that asserts the content (DAG root CID) exists in the parts (CAR CIDs).')
.option('-o, --output', 'Write output to this file.')
.option('-e, --expire', 'Duration after which claim expires e.g \'1min\' or \'1hr\'', '30s')
.example('partition bafybeiefif6pfs25c6g5r4lcvsk7l6f3vnnsuziitrlca3g6rhjkhmysna bagbaierae3n6cey3feykv3h5imue3eustl656dajifuddj3zedhpdofje3za -o partition.claim')
.action(async (contentArg, partArg, opts) => {
const content = Link.parse(contentArg)
const parts = [partArg, ...opts._].map(p => Link.parse(p).toV1())
const signer = getSigner()
const invocation = Assert.partition.invoke({
issuer: signer,
audience: servicePrincipal,
with: signer.did(),
nb: { content, parts },
expiration: toUcanExpiration(opts.expire)
})
await archiveClaim(invocation, opts.output)
})
.command('inclusion <content> <includes>')
.describe('Generate an inclusion claim that asserts the content (CAR CID) includes the blocks in the index (CARv2 index CID).')
.option('-o, --output', 'Write output to this file.')
.option('-e, --expire', 'Duration after which claim expires e.g \'1min\' or \'1hr\'', '30s')
.example('inclusion bagbaierae3n6cey3feykv3h5imue3eustl656dajifuddj3zedhpdofje3za bafkreihyikwmd6vlp5g6snhqipvigffx3w52l322dtqlrf4phanxisa34m -o inclusion.claim')
.action(async (contentArg, includesArg, opts) => {
const content = Link.parse(contentArg)
const includes = Link.parse(includesArg).toV1()
const signer = getSigner()
const invocation = Assert.inclusion.invoke({
issuer: signer,
audience: servicePrincipal,
with: signer.did(),
nb: { content, includes },
expiration: toUcanExpiration(opts.expire)
})
await archiveClaim(invocation, opts.output)
})
.command('index <content> <index>')
.describe('Generate an index claim that asserts a content graph can be found in blob(s) that are identified and indexed in the given index CID.')
.option('-o, --output', 'Write output to this file.')
.option('-e, --expire', 'Duration after which claim expires e.g \'1min\' or \'1hr\'', '30s')
.example('index bafyreib7pboydxne2smyrq2lhtgw6y3jcvutzsyhoti7qs3ci6q45x7cky bagbaiera7hndiywftjuayz44kxl2l3skjquhizgnycykc2lhzbhuxtribwaq -o index.claim')
.action(async (contentArg, includesArg, opts) => {
const content = Link.parse(contentArg)
const index = Link.parse(includesArg).toV1()
const signer = getSigner()
const invocation = Assert.index.invoke({
issuer: signer,
audience: servicePrincipal,
with: signer.did(),
nb: { content, index },
expiration: toUcanExpiration(opts.expire)
})
await archiveClaim(invocation, opts.output)
})
.command('relation <content>')
.describe('Generate a relation claim that asserts the content (block CID) links directly to the child (block CID).')
.option('-c, --child', 'One or more child CIDs that this content links to.')
.option('-p, --part', 'One or more CAR CIDs where the content and it\'s children may be found.')
.option('-i, --includes', 'One or more CARv2 CIDs corresponding to the parts.')
.option('-a, --includes-part', 'One or more CAR CIDs where the inclusion CID may be found.')
.option('-o, --output', 'Write output to this file.')
.option('-e, --expire', 'Duration after which claim expires e.g \'1min\' or \'1hr\'', '30s')
.example('relation bagbaierae3n6cey3feykv3h5imue3eustl656dajifuddj3zedhpdofje3za --child bafkreihyikwmd6vlp5g6snhqipvigffx3w52l322dtqlrf4phanxisa34m --part -o relation.claim')
.action(async (contentArg, opts) => {
const content = Link.parse(contentArg)
/** @type {import('multiformats/link').UnknownLink[]} */
const children = (Array.isArray(opts.child) ? opts.child : [opts.child]).map(c => Link.parse(c))
/** @type {import('multiformats/link').Link[]} */
const partContents = (Array.isArray(opts.part) ? opts.part : [opts.part]).map(p => Link.parse(p))
/** @type {import('multiformats/link').Link[]} */
const partIncludes = (Array.isArray(opts.includes) ? opts.includes : [opts.includes]).filter(Boolean).map(i => Link.parse(i))
/** @type {import('multiformats/link').Link[]} */
const partIncludesPart = (Array.isArray(opts['includes-part']) ? opts['includes-part'] : [opts['includes-part']]).filter(Boolean).map(i => Link.parse(i))
const parts = partContents.map((content, i) => {
const includes = partIncludes[i]
if (!includes) return { content }
const includesPart = partIncludesPart[i]
if (!includesPart) return { content, includes: { content: includes } }
return { content, includes: { content: includes, parts: [includesPart] } }
})
const signer = getSigner()
const invocation = Assert.relation.invoke({
issuer: signer,
audience: servicePrincipal,
with: signer.did(),
nb: { content, children, parts },
expiration: toUcanExpiration(opts.expire)
})
await archiveClaim(invocation, opts.output)
})
.command('equals <content> <equal>')
.describe('Generate an equals claim that asserts the content is referred to by another CID and/or multihash.')
.option('-o, --output', 'Write output to this file.')
.option('-e, --expire', 'Duration after which claim expires e.g \'1min\' or \'1hr\'', '30s')
.example('equals QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn bafybeiczsscdsbs7ffqz55asqdf3smv6klcw3gofszvwlyarci47bgf354 -o equals.claim')
.action(async (contentArg, equalsArg, opts) => {
const content = Link.parse(contentArg)
const equals = Link.parse(equalsArg)
const signer = getSigner()
const invocation = Assert.equals.invoke({
issuer: signer,
audience: servicePrincipal,
with: signer.did(),
nb: { content, equals },
expiration: toUcanExpiration(opts.expire)
})
await archiveClaim(invocation, opts.output)
})
.command('inspect <claim>')
.describe('Inspect a generated claim.')
.action(async path => {
const archive = await fs.promises.readFile(path)
const delegation = await Delegation.extract(archive)
if (!delegation.ok) throw new Error('failed to extract archive', { cause: delegation.error })
console.log(inspect(JSON.parse(JSON.stringify(delegation.ok)), false, Infinity, process.stdout.isTTY))
})
.command('write <claim>')
.describe('Write claims to claims.web3.storage.')
.action(async (path, opts) => {
const paths = [path, ...opts._]
const invocations = []
for (const path of paths) {
const archive = await fs.promises.readFile(path)
const delegation = await Delegation.extract(archive)
if (!delegation.ok) throw new Error('failed to extract archive', { cause: delegation.error })
invocations.push(delegation.ok)
}
// @ts-expect-error
const results = await connection.execute(invocations[0], ...invocations.slice(1))
// @ts-expect-error
for (const result of results) {
console.log(result.out)
}
})
.command('read <content>')
.describe('Read claims from claims.web3.storage.')
.option('-w, --walk', 'Walk these properties encountered in claims.')
.option('--verbose', 'Write claim information to stderr.')
.option('-o, --output', 'Write output to this file.')
.action(async (contentArg, opts) => {
const content = Link.parse(contentArg)
const walk = Array.isArray(opts.walk) ? opts.walk : opts.walk?.split(',')
const res = await Client.fetch(content.multihash, { walk, serviceURL })
if (!res.ok) throw new Error(`unexpected service status: ${res.status}`, { cause: await res.text() })
if (!res.body) throw new Error('missing response body')
const writable = Writable.toWeb(opts.output ? fs.createWriteStream(opts.output) : process.stdout)
if (opts.verbose) {
const [body0, body1] = res.body.tee()
await Promise.all([
body0.pipeTo(writable),
body1
.pipeThrough(new CARReaderStream())
.pipeTo(new WritableStream({
async write (block) {
const claim = await Client.decode(block.bytes)
const raw = await Delegation.extract(block.bytes)
if (raw.error) throw new Error(`failed to decode claim for: ${claim.content}`, { cause: raw.error })
console.warn(inspect(JSON.parse(JSON.stringify(raw.ok)), false, Infinity, process.stdout.isTTY))
}
}))
])
} else {
await res.body.pipeTo(writable)
}
})
/**
* @param {import('@ucanto/principal/ed25519').IssuedInvocationView} invocation
* @param {string} [outputPath]
*/
const archiveClaim = async (invocation, outputPath) => {
const ipldView = await invocation.buildIPLDView()
const archive = await ipldView.archive()
if (!archive.ok) throw new Error('failed to archive invocation', { cause: archive.error })
if (outputPath) {
await fs.promises.writeFile(outputPath, archive.ok)
console.warn(ipldView.cid.toString())
} else {
process.stdout.write(archive.ok)
}
}
const getSigner = () => {
const pk = process.env.PRIVATE_KEY
if (!pk) throw new Error('missing PRIVATE_KEY environment variable')
return ed25519.parse(pk).withDID(servicePrincipal.did())
}
/**
* take a human duration like 1d (one day)
* return "the number of integer seconds since the Unix epoch."
* > https://github.com/ucan-wg/spec#323-time-bounds
* @param {string} str
*/
function toUcanExpiration (str, now = UCAN.now()) {
if (!str) return undefined
const input = str.toLocaleLowerCase()
if (input === 'never') {
return Infinity
}
const durationInSeconds = duration(str, 's')
if (durationInSeconds === undefined) {
return undefined
}
return now + durationInSeconds
}
prog.parse(process.argv)