@stream44.studio/t44-blockchaincommons.com 0.1.0-rc.2

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.
Files changed (37) hide show
  1. package/.dco-signatures +9 -0
  2. package/.github/workflows/dco.yml +12 -0
  3. package/.github/workflows/gordian-open-integrity.yml +13 -0
  4. package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
  5. package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
  6. package/.o/GordianOpenIntegrity.yaml +25 -0
  7. package/DCO.md +34 -0
  8. package/README.md +210 -0
  9. package/action.yml +47 -0
  10. package/bin/oi +152 -0
  11. package/caps/GordianOpenIntegrity.test.ts +879 -0
  12. package/caps/GordianOpenIntegrity.ts +821 -0
  13. package/caps/XidDocumentLedger.test.ts +687 -0
  14. package/caps/XidDocumentLedger.ts +545 -0
  15. package/caps/__snapshots__/XidDocumentLedger.test.ts.snap +11 -0
  16. package/caps/__snapshots__/XidLedger.test.ts.snap +11 -0
  17. package/caps/lifehash.test.ts +302 -0
  18. package/caps/lifehash.ts +142 -0
  19. package/caps/open-integrity-js.test.ts +252 -0
  20. package/caps/open-integrity-js.ts +485 -0
  21. package/caps/open-integrity-sh.test.ts +188 -0
  22. package/caps/open-integrity-sh.ts +187 -0
  23. package/caps/open-integrity.test.ts +259 -0
  24. package/caps/provenance-mark-cli.test.ts +387 -0
  25. package/caps/provenance-mark-cli.ts +174 -0
  26. package/caps/provenance-mark.test.ts +233 -0
  27. package/caps/provenance-mark.ts +223 -0
  28. package/caps/xid.test.ts +828 -0
  29. package/caps/xid.ts +565 -0
  30. package/examples/01-XID-DocumentLedger/__snapshots__/main.test.ts.snap +10 -0
  31. package/examples/01-XID-DocumentLedger/main.test.ts +182 -0
  32. package/examples/02-XID-Rotate-InceptionKey/__snapshots__/main.test.ts.snap +53 -0
  33. package/examples/02-XID-Rotate-InceptionKey/main.test.ts +232 -0
  34. package/examples/03-GordianOpenIntegrity/main.test.ts +176 -0
  35. package/examples/04-GordianOpenIntegrityCli/main.test.ts +119 -0
  36. package/package.json +37 -0
  37. package/tsconfig.json +28 -0
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env bun test
2
+
3
+ import * as bunTest from 'bun:test'
4
+ import { run } from 't44/workspace-rt'
5
+ import { join } from 'path'
6
+ import { rm, mkdir, writeFile } from 'fs/promises'
7
+
8
+ const WORK_DIR = join(import.meta.dir, '.~lifehash')
9
+
10
+ const {
11
+ test: { describe, it, expect },
12
+ lifehash,
13
+ provenanceMark,
14
+ } = await run(async ({ encapsulate, CapsulePropertyTypes, makeImportStack }: any) => {
15
+ const spine = await encapsulate({
16
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
17
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
18
+ '#': {
19
+ test: {
20
+ type: CapsulePropertyTypes.Mapping,
21
+ value: 't44/caps/WorkspaceTest',
22
+ options: {
23
+ '#': {
24
+ bunTest,
25
+ env: {}
26
+ }
27
+ }
28
+ },
29
+ lifehash: {
30
+ type: CapsulePropertyTypes.Mapping,
31
+ value: './lifehash'
32
+ },
33
+ provenanceMark: {
34
+ type: CapsulePropertyTypes.Mapping,
35
+ value: './provenance-mark'
36
+ },
37
+ }
38
+ }
39
+ }, {
40
+ importMeta: import.meta,
41
+ importStack: makeImportStack(),
42
+ capsuleName: 't44/caps/providers/blockchaincommons.com/lifehash.test'
43
+ })
44
+ return { spine }
45
+ }, async ({ spine, apis }: any) => {
46
+ return apis[spine.capsuleSourceLineRef]
47
+ }, {
48
+ importMeta: import.meta
49
+ })
50
+
51
+ await rm(WORK_DIR, { recursive: true, force: true })
52
+ await mkdir(WORK_DIR, { recursive: true })
53
+
54
+ describe('LifeHash Capsule', function () {
55
+
56
+ // ──────────────────────────────────────────────────────────────────
57
+ // 1. Basic image generation
58
+ // ──────────────────────────────────────────────────────────────────
59
+
60
+ describe('1. makeFromUtf8', function () {
61
+
62
+ it('should generate an image from a string', async function () {
63
+ const image = await lifehash.makeFromUtf8({ input: 'hello world' })
64
+ expect(image.width).toBe(32)
65
+ expect(image.height).toBe(32)
66
+ expect(image.colors).toBeInstanceOf(Uint8Array)
67
+ expect(image.colors.length).toBe(32 * 32 * 3)
68
+
69
+ const ppm = await lifehash.toPPM({ image })
70
+ await writeFile(join(WORK_DIR, 'hello-world.ppm'), ppm)
71
+ const svg = await lifehash.toSVG({ image })
72
+ await writeFile(join(WORK_DIR, 'hello-world.svg'), svg)
73
+ })
74
+
75
+ it('should generate a larger image with moduleSize', async function () {
76
+ const image = await lifehash.makeFromUtf8({ input: 'hello world', moduleSize: 4 })
77
+ expect(image.width).toBe(128)
78
+ expect(image.height).toBe(128)
79
+ expect(image.colors.length).toBe(128 * 128 * 3)
80
+ })
81
+
82
+ it('should generate an image with alpha channel', async function () {
83
+ const image = await lifehash.makeFromUtf8({ input: 'hello world', hasAlpha: true })
84
+ expect(image.colors.length).toBe(32 * 32 * 4)
85
+ })
86
+
87
+ it('should produce deterministic output for the same input', async function () {
88
+ const image1 = await lifehash.makeFromUtf8({ input: 'deterministic' })
89
+ const image2 = await lifehash.makeFromUtf8({ input: 'deterministic' })
90
+ expect(image1.colors).toEqual(image2.colors)
91
+ })
92
+
93
+ it('should produce different output for different inputs', async function () {
94
+ const image1 = await lifehash.makeFromUtf8({ input: 'alpha' })
95
+ const image2 = await lifehash.makeFromUtf8({ input: 'beta' })
96
+ expect(image1.colors).not.toEqual(image2.colors)
97
+ })
98
+ })
99
+
100
+ // ──────────────────────────────────────────────────────────────────
101
+ // 2. Version variants
102
+ // ──────────────────────────────────────────────────────────────────
103
+
104
+ describe('2. Version variants', function () {
105
+
106
+ it('should generate version1 image', async function () {
107
+ const image = await lifehash.makeFromUtf8({ input: 'test', version: 'version1' })
108
+ expect(image.width).toBe(32)
109
+ expect(image.height).toBe(32)
110
+ await writeFile(join(WORK_DIR, 'version1.svg'), await lifehash.toSVG({ image }))
111
+ })
112
+
113
+ it('should generate detailed image (32x32)', async function () {
114
+ const image = await lifehash.makeFromUtf8({ input: 'test', version: 'detailed' })
115
+ expect(image.width).toBe(64)
116
+ expect(image.height).toBe(64)
117
+ await writeFile(join(WORK_DIR, 'detailed.svg'), await lifehash.toSVG({ image }))
118
+ })
119
+
120
+ it('should generate fiducial image (32x32)', async function () {
121
+ const image = await lifehash.makeFromUtf8({ input: 'test', version: 'fiducial' })
122
+ expect(image.width).toBe(32)
123
+ expect(image.height).toBe(32)
124
+ await writeFile(join(WORK_DIR, 'fiducial.svg'), await lifehash.toSVG({ image }))
125
+ })
126
+
127
+ it('should generate grayscale fiducial image', async function () {
128
+ const image = await lifehash.makeFromUtf8({ input: 'test', version: 'grayscale_fiducial' })
129
+ expect(image.width).toBe(32)
130
+ expect(image.height).toBe(32)
131
+ await writeFile(join(WORK_DIR, 'grayscale-fiducial.svg'), await lifehash.toSVG({ image }))
132
+ })
133
+
134
+ it('should produce different images for different versions', async function () {
135
+ const v1 = await lifehash.makeFromUtf8({ input: 'same', version: 'version1' })
136
+ const v2 = await lifehash.makeFromUtf8({ input: 'same', version: 'version2' })
137
+ expect(v1.colors).not.toEqual(v2.colors)
138
+ })
139
+ })
140
+
141
+ // ──────────────────────────────────────────────────────────────────
142
+ // 3. makeFromData
143
+ // ──────────────────────────────────────────────────────────────────
144
+
145
+ describe('3. makeFromData', function () {
146
+
147
+ it('should generate an image from raw bytes', async function () {
148
+ const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])
149
+ const image = await lifehash.makeFromData({ data })
150
+ expect(image.width).toBe(32)
151
+ expect(image.height).toBe(32)
152
+ expect(image.colors.length).toBe(32 * 32 * 3)
153
+ })
154
+ })
155
+
156
+ // ──────────────────────────────────────────────────────────────────
157
+ // 4. makeFromDigest
158
+ // ──────────────────────────────────────────────────────────────────
159
+
160
+ describe('4. makeFromDigest', function () {
161
+
162
+ it('should generate an image from a 32-byte digest', async function () {
163
+ const { sha256 } = await lifehash.types()
164
+ const digest = sha256(new TextEncoder().encode('hello'))
165
+ expect(digest.length).toBe(32)
166
+
167
+ const image = await lifehash.makeFromDigest({ digest })
168
+ expect(image.width).toBe(32)
169
+ expect(image.height).toBe(32)
170
+ })
171
+
172
+ it('should reject a non-32-byte digest', async function () {
173
+ const badDigest = new Uint8Array(16)
174
+ await expect(lifehash.makeFromDigest({ digest: badDigest })).rejects.toThrow('32 bytes')
175
+ })
176
+ })
177
+
178
+ // ──────────────────────────────────────────────────────────────────
179
+ // 5. LifeHash from provenance mark
180
+ // ──────────────────────────────────────────────────────────────────
181
+
182
+ describe('5. LifeHash from provenance mark', function () {
183
+
184
+ let mark: any
185
+ let markIdentifier: string
186
+
187
+ it('should create a provenance mark', async function () {
188
+ const generator = await provenanceMark.createGenerator({
189
+ type: 'passphrase',
190
+ passphrase: 'lifehash-test-chain',
191
+ })
192
+ mark = await provenanceMark.nextMark({
193
+ generator,
194
+ date: new Date(Date.UTC(2025, 0, 1)),
195
+ })
196
+ expect(mark).toBeDefined()
197
+ })
198
+
199
+ it('should get mark identifier string', async function () {
200
+ markIdentifier = await provenanceMark.getIdentifier({ mark })
201
+ expect(typeof markIdentifier).toBe('string')
202
+ expect(markIdentifier.length).toBeGreaterThan(0)
203
+ })
204
+
205
+ it('should generate a lifehash image from the mark identifier', async function () {
206
+ const image = await lifehash.makeFromUtf8({ input: markIdentifier })
207
+ expect(image.width).toBe(32)
208
+ expect(image.height).toBe(32)
209
+ expect(image.colors).toBeInstanceOf(Uint8Array)
210
+ expect(image.colors.length).toBe(32 * 32 * 3)
211
+
212
+ await writeFile(join(WORK_DIR, 'provenance-mark-identifier.svg'), await lifehash.toSVG({ image }))
213
+ await writeFile(join(WORK_DIR, 'provenance-mark-identifier.ppm'), await lifehash.toPPM({ image }))
214
+ })
215
+
216
+ it('should generate a lifehash from the mark hash bytes', async function () {
217
+ const hashBytes = await provenanceMark.getHash({ mark })
218
+ expect(hashBytes).toBeInstanceOf(Uint8Array)
219
+
220
+ const image = await lifehash.makeFromData({ data: hashBytes })
221
+ expect(image.width).toBe(32)
222
+ expect(image.height).toBe(32)
223
+
224
+ await writeFile(join(WORK_DIR, 'provenance-mark-hash.svg'), await lifehash.toSVG({ image }))
225
+ })
226
+
227
+ it('should produce a deterministic lifehash for the same mark', async function () {
228
+ const image1 = await lifehash.makeFromUtf8({ input: markIdentifier })
229
+ const image2 = await lifehash.makeFromUtf8({ input: markIdentifier })
230
+ expect(image1.colors).toEqual(image2.colors)
231
+ })
232
+
233
+ it('should produce different lifehashes for different marks', async function () {
234
+ const generator = await provenanceMark.createGenerator({
235
+ type: 'passphrase',
236
+ passphrase: 'different-chain',
237
+ })
238
+ const otherMark = await provenanceMark.nextMark({
239
+ generator,
240
+ date: new Date(Date.UTC(2025, 0, 1)),
241
+ })
242
+ const otherIdentifier = await provenanceMark.getIdentifier({ mark: otherMark })
243
+
244
+ const image1 = await lifehash.makeFromUtf8({ input: markIdentifier })
245
+ const image2 = await lifehash.makeFromUtf8({ input: otherIdentifier })
246
+ expect(image1.colors).not.toEqual(image2.colors)
247
+ })
248
+ })
249
+
250
+ // ──────────────────────────────────────────────────────────────────
251
+ // 6. Output formats
252
+ // ──────────────────────────────────────────────────────────────────
253
+
254
+ describe('6. Output formats', function () {
255
+
256
+ it('should convert image to PPM format', async function () {
257
+ const image = await lifehash.makeFromUtf8({ input: 'ppm-test' })
258
+ const ppm = await lifehash.toPPM({ image })
259
+ expect(ppm).toBeInstanceOf(Uint8Array)
260
+
261
+ const header = new TextDecoder().decode(ppm.slice(0, 20))
262
+ expect(header.startsWith('P6\n32 32\n255\n')).toBe(true)
263
+
264
+ const headerLength = new TextEncoder().encode('P6\n32 32\n255\n').length
265
+ expect(ppm.length).toBe(headerLength + 32 * 32 * 3)
266
+ })
267
+
268
+ it('should convert image to SVG format', async function () {
269
+ const image = await lifehash.makeFromUtf8({ input: 'svg-test' })
270
+ const svg = await lifehash.toSVG({ image })
271
+ expect(typeof svg).toBe('string')
272
+ expect(svg).toContain('<svg')
273
+ expect(svg).toContain('viewBox="0 0 32 32"')
274
+ expect(svg).toContain('</svg>')
275
+ expect(svg).toContain('<rect')
276
+ expect(svg).toContain('fill="rgb(')
277
+ })
278
+
279
+ it('should produce valid SVG for scaled images', async function () {
280
+ const image = await lifehash.makeFromUtf8({ input: 'svg-scaled', moduleSize: 2 })
281
+ const svg = await lifehash.toSVG({ image })
282
+ expect(svg).toContain('viewBox="0 0 64 64"')
283
+ })
284
+ })
285
+
286
+ // ──────────────────────────────────────────────────────────────────
287
+ // 7. Types exposure
288
+ // ──────────────────────────────────────────────────────────────────
289
+
290
+ describe('7. Types', function () {
291
+
292
+ it('should expose library types', async function () {
293
+ const types = await lifehash.types()
294
+ expect(types.Version).toBeDefined()
295
+ expect(types.Pattern).toBeDefined()
296
+ expect(types.sha256).toBeDefined()
297
+ expect(types.dataToHex).toBeDefined()
298
+ expect(types.hexToData).toBeDefined()
299
+ })
300
+ })
301
+
302
+ })
@@ -0,0 +1,142 @@
1
+
2
+
3
+ import {
4
+ makeFromUtf8,
5
+ makeFromData,
6
+ makeFromDigest,
7
+ Version,
8
+ Pattern,
9
+ type Image,
10
+ dataToHex,
11
+ hexToData,
12
+ sha256,
13
+ Color,
14
+ Size,
15
+ } from '@bcts/lifehash'
16
+
17
+
18
+ export async function capsule({
19
+ encapsulate,
20
+ CapsulePropertyTypes,
21
+ makeImportStack
22
+ }: {
23
+ encapsulate: any
24
+ CapsulePropertyTypes: any
25
+ makeImportStack: any
26
+ }) {
27
+
28
+ return encapsulate({
29
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
30
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
31
+ '#': {
32
+
33
+ makeFromUtf8: {
34
+ type: CapsulePropertyTypes.Function,
35
+ value: async function (this: any, context: {
36
+ input: string
37
+ version?: 'version1' | 'version2' | 'detailed' | 'fiducial' | 'grayscale_fiducial'
38
+ moduleSize?: number
39
+ hasAlpha?: boolean
40
+ }): Promise<Image> {
41
+ const version = context.version ? Version[context.version] : Version.version2
42
+ return makeFromUtf8(context.input, version, context.moduleSize ?? 1, context.hasAlpha ?? false)
43
+ }
44
+ },
45
+
46
+ makeFromData: {
47
+ type: CapsulePropertyTypes.Function,
48
+ value: async function (this: any, context: {
49
+ data: Uint8Array
50
+ version?: 'version1' | 'version2' | 'detailed' | 'fiducial' | 'grayscale_fiducial'
51
+ moduleSize?: number
52
+ hasAlpha?: boolean
53
+ }): Promise<Image> {
54
+ const version = context.version ? Version[context.version] : Version.version2
55
+ return makeFromData(context.data, version, context.moduleSize ?? 1, context.hasAlpha ?? false)
56
+ }
57
+ },
58
+
59
+ makeFromDigest: {
60
+ type: CapsulePropertyTypes.Function,
61
+ value: async function (this: any, context: {
62
+ digest: Uint8Array
63
+ version?: 'version1' | 'version2' | 'detailed' | 'fiducial' | 'grayscale_fiducial'
64
+ moduleSize?: number
65
+ hasAlpha?: boolean
66
+ }): Promise<Image> {
67
+ const version = context.version ? Version[context.version] : Version.version2
68
+ return makeFromDigest(context.digest, version, context.moduleSize ?? 1, context.hasAlpha ?? false)
69
+ }
70
+ },
71
+
72
+ toPPM: {
73
+ type: CapsulePropertyTypes.Function,
74
+ value: async function (this: any, context: {
75
+ image: Image
76
+ }): Promise<Uint8Array> {
77
+ const { width, height, colors } = context.image
78
+ const header = `P6\n${width} ${height}\n255\n`
79
+ const headerBytes = new TextEncoder().encode(header)
80
+ const pixelCount = width * height
81
+ const pixelData = new Uint8Array(pixelCount * 3)
82
+ const channels = colors.length / (width * height)
83
+ for (let i = 0; i < pixelCount; i++) {
84
+ pixelData[i * 3] = colors[i * channels]
85
+ pixelData[i * 3 + 1] = colors[i * channels + 1]
86
+ pixelData[i * 3 + 2] = colors[i * channels + 2]
87
+ }
88
+ const result = new Uint8Array(headerBytes.length + pixelData.length)
89
+ result.set(headerBytes)
90
+ result.set(pixelData, headerBytes.length)
91
+ return result
92
+ }
93
+ },
94
+
95
+ toSVG: {
96
+ type: CapsulePropertyTypes.Function,
97
+ value: async function (this: any, context: {
98
+ image: Image
99
+ }): Promise<string> {
100
+ const { width, height, colors } = context.image
101
+ const channels = colors.length / (width * height)
102
+ const lines: string[] = []
103
+ lines.push(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" shape-rendering="crispEdges">`)
104
+ for (let y = 0; y < height; y++) {
105
+ for (let x = 0; x < width; x++) {
106
+ const i = (y * width + x) * channels
107
+ const r = colors[i]
108
+ const g = colors[i + 1]
109
+ const b = colors[i + 2]
110
+ lines.push(`<rect x="${x}" y="${y}" width="1" height="1" fill="rgb(${r},${g},${b})"/>`)
111
+ }
112
+ }
113
+ lines.push('</svg>')
114
+ return lines.join('\n')
115
+ }
116
+ },
117
+
118
+ // Expose library types and utilities for convenience
119
+ types: {
120
+ type: CapsulePropertyTypes.Function,
121
+ value: async function (this: any) {
122
+ return {
123
+ Version,
124
+ Pattern,
125
+ Color,
126
+ Size,
127
+ dataToHex,
128
+ hexToData,
129
+ sha256,
130
+ }
131
+ }
132
+ }
133
+
134
+ }
135
+ }
136
+ }, {
137
+ importMeta: import.meta,
138
+ importStack: makeImportStack(),
139
+ capsuleName: capsule['#'],
140
+ })
141
+ }
142
+ capsule['#'] = 't44/caps/providers/blockchaincommons.com/lifehash'