@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.
- package/.dco-signatures +9 -0
- package/.github/workflows/dco.yml +12 -0
- package/.github/workflows/gordian-open-integrity.yml +13 -0
- package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity.yaml +25 -0
- package/DCO.md +34 -0
- package/README.md +210 -0
- package/action.yml +47 -0
- package/bin/oi +152 -0
- package/caps/GordianOpenIntegrity.test.ts +879 -0
- package/caps/GordianOpenIntegrity.ts +821 -0
- package/caps/XidDocumentLedger.test.ts +687 -0
- package/caps/XidDocumentLedger.ts +545 -0
- package/caps/__snapshots__/XidDocumentLedger.test.ts.snap +11 -0
- package/caps/__snapshots__/XidLedger.test.ts.snap +11 -0
- package/caps/lifehash.test.ts +302 -0
- package/caps/lifehash.ts +142 -0
- package/caps/open-integrity-js.test.ts +252 -0
- package/caps/open-integrity-js.ts +485 -0
- package/caps/open-integrity-sh.test.ts +188 -0
- package/caps/open-integrity-sh.ts +187 -0
- package/caps/open-integrity.test.ts +259 -0
- package/caps/provenance-mark-cli.test.ts +387 -0
- package/caps/provenance-mark-cli.ts +174 -0
- package/caps/provenance-mark.test.ts +233 -0
- package/caps/provenance-mark.ts +223 -0
- package/caps/xid.test.ts +828 -0
- package/caps/xid.ts +565 -0
- package/examples/01-XID-DocumentLedger/__snapshots__/main.test.ts.snap +10 -0
- package/examples/01-XID-DocumentLedger/main.test.ts +182 -0
- package/examples/02-XID-Rotate-InceptionKey/__snapshots__/main.test.ts.snap +53 -0
- package/examples/02-XID-Rotate-InceptionKey/main.test.ts +232 -0
- package/examples/03-GordianOpenIntegrity/main.test.ts +176 -0
- package/examples/04-GordianOpenIntegrityCli/main.test.ts +119 -0
- package/package.json +37 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
|
|
2
|
+
import { $ } from 'bun'
|
|
3
|
+
import { join, dirname, resolve } from 'path'
|
|
4
|
+
import { chmod, writeFile, mkdir } from 'fs/promises'
|
|
5
|
+
import { existsSync } from 'fs'
|
|
6
|
+
import { fileURLToPath } from 'url'
|
|
7
|
+
import { tmpdir } from 'os'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
function resolveCoreSrcDir(): string {
|
|
11
|
+
const thisDir = dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
// Walk up from caps/providers/blockchaincommons.com/ to the package root
|
|
13
|
+
const pkgRoot = resolve(thisDir, '..')
|
|
14
|
+
return join(pkgRoot, 'node_modules', 'open-integrity-core', 'src')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const gitSigningDir = join(tmpdir(), '.open-integrity-sh-signing')
|
|
18
|
+
const gitSigningKeyPath = join(gitSigningDir, 'signing_key_ed25519')
|
|
19
|
+
const gitSigningConfigPath = join(gitSigningDir, 'gitconfig')
|
|
20
|
+
const gitAllowedSignersPath = join(gitSigningDir, 'allowed_signers')
|
|
21
|
+
|
|
22
|
+
async function ensureGitSigningConfig(): Promise<string> {
|
|
23
|
+
if (existsSync(gitSigningConfigPath)) return gitSigningConfigPath
|
|
24
|
+
await mkdir(gitSigningDir, { recursive: true })
|
|
25
|
+
if (!existsSync(gitSigningKeyPath)) {
|
|
26
|
+
const proc = Bun.spawn(['ssh-keygen', '-t', 'ed25519', '-f', gitSigningKeyPath, '-N', '', '-q'], {
|
|
27
|
+
stdout: 'ignore', stderr: 'ignore',
|
|
28
|
+
})
|
|
29
|
+
await proc.exited
|
|
30
|
+
}
|
|
31
|
+
// Read the public key to add to allowed_signers
|
|
32
|
+
const pubKey = (await Bun.file(gitSigningKeyPath + '.pub').text()).trim()
|
|
33
|
+
await writeFile(gitAllowedSignersPath, `oi-sh@test.local ${pubKey}\n`)
|
|
34
|
+
const gitconfig = [
|
|
35
|
+
'[user]',
|
|
36
|
+
' name = Open Integrity SH',
|
|
37
|
+
' email = oi-sh@test.local',
|
|
38
|
+
` signingkey = ${gitSigningKeyPath}`,
|
|
39
|
+
'[gpg]',
|
|
40
|
+
' format = ssh',
|
|
41
|
+
'[gpg "ssh"]',
|
|
42
|
+
` allowedSignersFile = ${gitAllowedSignersPath}`,
|
|
43
|
+
'[commit]',
|
|
44
|
+
' gpgsign = true',
|
|
45
|
+
'[init]',
|
|
46
|
+
' defaultBranch = main',
|
|
47
|
+
].join('\n')
|
|
48
|
+
await writeFile(gitSigningConfigPath, gitconfig)
|
|
49
|
+
return gitSigningConfigPath
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function runScript(scriptPath: string, args: string[], options: { cwd: string; env?: Record<string, string> }): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
53
|
+
await chmod(scriptPath, 0o755).catch(() => { })
|
|
54
|
+
const configPath = await ensureGitSigningConfig()
|
|
55
|
+
const escaped = [scriptPath, ...args].map(a => $.escape(a)).join(' ')
|
|
56
|
+
// Set ZDOTDIR to a non-existent path so zsh skips .zshenv/.zshrc (which trigger bun/workspace activation)
|
|
57
|
+
// Set GIT_CONFIG_GLOBAL so the scripts find the required signing config
|
|
58
|
+
const env = { ...process.env, ZDOTDIR: '/tmp/.zsh-no-dotfiles', GIT_CONFIG_GLOBAL: configPath, ...options.env }
|
|
59
|
+
const result = await $`/bin/zsh --no-rcs ${{ raw: escaped }}`.cwd(options.cwd).env(env).nothrow().quiet()
|
|
60
|
+
return {
|
|
61
|
+
stdout: result.stdout.toString().trim(),
|
|
62
|
+
stderr: result.stderr.toString().trim(),
|
|
63
|
+
exitCode: result.exitCode,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const srcDir = resolveCoreSrcDir()
|
|
68
|
+
const SETUP_SCRIPT = join(srcDir, 'setup_git_inception_repo.sh')
|
|
69
|
+
const GET_DID_SCRIPT = join(srcDir, 'get_repo_did.sh')
|
|
70
|
+
const AUDIT_SCRIPT = join(srcDir, 'audit_inception_commit-POC.sh')
|
|
71
|
+
const SNIPPET_SCRIPT = join(srcDir, 'snippet_template.sh')
|
|
72
|
+
|
|
73
|
+
export async function capsule({
|
|
74
|
+
encapsulate,
|
|
75
|
+
CapsulePropertyTypes,
|
|
76
|
+
makeImportStack
|
|
77
|
+
}: {
|
|
78
|
+
encapsulate: any
|
|
79
|
+
CapsulePropertyTypes: any
|
|
80
|
+
makeImportStack: any
|
|
81
|
+
}) {
|
|
82
|
+
return encapsulate({
|
|
83
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
84
|
+
'#@stream44.studio/encapsulate/structs/Capsule': {},
|
|
85
|
+
'#': {
|
|
86
|
+
|
|
87
|
+
createInceptionRepo: {
|
|
88
|
+
type: CapsulePropertyTypes.Function,
|
|
89
|
+
value: async function (this: any, context: {
|
|
90
|
+
repoDir: string
|
|
91
|
+
force?: boolean
|
|
92
|
+
}) {
|
|
93
|
+
const args = ['--repo', context.repoDir]
|
|
94
|
+
if (context.force) {
|
|
95
|
+
args.push('--force')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const result = await runScript(SETUP_SCRIPT, args, { cwd: dirname(context.repoDir) })
|
|
99
|
+
|
|
100
|
+
if (result.exitCode !== 0) {
|
|
101
|
+
throw new Error(`setup_git_inception_repo.sh failed (exit ${result.exitCode}): ${result.stderr}`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
exitCode: result.exitCode,
|
|
106
|
+
stdout: result.stdout,
|
|
107
|
+
stderr: result.stderr,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
getRepoDid: {
|
|
113
|
+
type: CapsulePropertyTypes.Function,
|
|
114
|
+
value: async function (this: any, context: {
|
|
115
|
+
repoDir: string
|
|
116
|
+
}) {
|
|
117
|
+
const result = await runScript(GET_DID_SCRIPT, ['-C', context.repoDir], { cwd: context.repoDir })
|
|
118
|
+
|
|
119
|
+
if (result.exitCode !== 0) {
|
|
120
|
+
throw new Error(`get_repo_did.sh failed (exit ${result.exitCode}): ${result.stderr}`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return result.stdout.trim()
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
auditInceptionCommit: {
|
|
128
|
+
type: CapsulePropertyTypes.Function,
|
|
129
|
+
value: async function (this: any, context: {
|
|
130
|
+
repoDir: string
|
|
131
|
+
verbose?: boolean
|
|
132
|
+
quiet?: boolean
|
|
133
|
+
noPrompt?: boolean
|
|
134
|
+
noColor?: boolean
|
|
135
|
+
}) {
|
|
136
|
+
const args = ['-C', context.repoDir]
|
|
137
|
+
if (context.verbose) args.push('--verbose')
|
|
138
|
+
if (context.quiet) args.push('--quiet')
|
|
139
|
+
if (context.noPrompt) args.push('--no-prompt')
|
|
140
|
+
if (context.noColor) args.push('--no-color')
|
|
141
|
+
|
|
142
|
+
const result = await runScript(AUDIT_SCRIPT, args, { cwd: context.repoDir })
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
passed: result.exitCode === 0,
|
|
146
|
+
exitCode: result.exitCode,
|
|
147
|
+
stdout: result.stdout,
|
|
148
|
+
stderr: result.stderr,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
showFileStatus: {
|
|
154
|
+
type: CapsulePropertyTypes.Function,
|
|
155
|
+
value: async function (this: any, context: {
|
|
156
|
+
filePath: string
|
|
157
|
+
format?: 'default' | 'json' | 'yaml'
|
|
158
|
+
}) {
|
|
159
|
+
const args: string[] = []
|
|
160
|
+
if (context.format) {
|
|
161
|
+
args.push('--format', context.format)
|
|
162
|
+
}
|
|
163
|
+
args.push(context.filePath)
|
|
164
|
+
|
|
165
|
+
const result = await runScript(SNIPPET_SCRIPT, args, { cwd: dirname(context.filePath) })
|
|
166
|
+
|
|
167
|
+
if (result.exitCode !== 0) {
|
|
168
|
+
throw new Error(`snippet_template.sh failed (exit ${result.exitCode}): ${result.stderr}`)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
exitCode: result.exitCode,
|
|
173
|
+
stdout: result.stdout,
|
|
174
|
+
stderr: result.stderr,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}, {
|
|
182
|
+
importMeta: import.meta,
|
|
183
|
+
importStack: makeImportStack(),
|
|
184
|
+
capsuleName: capsule['#'],
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
capsule['#'] = 't44/caps/providers/blockchaincommons.com/open-integrity-sh'
|
|
@@ -0,0 +1,259 @@
|
|
|
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, readFile } from 'fs/promises'
|
|
7
|
+
|
|
8
|
+
const WORK_DIR = join(import.meta.dir, '.~open-integrity')
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
test: { describe, it, expect },
|
|
12
|
+
js,
|
|
13
|
+
sh
|
|
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
|
+
js: {
|
|
30
|
+
type: CapsulePropertyTypes.Mapping,
|
|
31
|
+
value: './open-integrity-js'
|
|
32
|
+
},
|
|
33
|
+
sh: {
|
|
34
|
+
type: CapsulePropertyTypes.Mapping,
|
|
35
|
+
value: './open-integrity-sh'
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}, {
|
|
40
|
+
importMeta: import.meta,
|
|
41
|
+
importStack: makeImportStack(),
|
|
42
|
+
capsuleName: 't44/caps/providers/blockchaincommons.com/open-integrity.test'
|
|
43
|
+
})
|
|
44
|
+
return { spine }
|
|
45
|
+
}, async ({ spine, apis }: any) => {
|
|
46
|
+
return apis[spine.capsuleSourceLineRef]
|
|
47
|
+
}, {
|
|
48
|
+
importMeta: import.meta
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// Clean up before tests
|
|
52
|
+
await rm(WORK_DIR, { recursive: true, force: true })
|
|
53
|
+
await mkdir(WORK_DIR, { recursive: true })
|
|
54
|
+
|
|
55
|
+
describe('Open Integrity: JS vs SH Cross-Validation', function () {
|
|
56
|
+
|
|
57
|
+
const keysDir = join(WORK_DIR, 'keys')
|
|
58
|
+
const shGenRepoDir = join(WORK_DIR, 'repo-sh-gen')
|
|
59
|
+
const jsGenRepoDir = join(WORK_DIR, 'repo-js-gen')
|
|
60
|
+
const jsOnlyRepoDir = join(WORK_DIR, 'repo-js-only')
|
|
61
|
+
|
|
62
|
+
let aliceKey: any
|
|
63
|
+
let bobKey: any
|
|
64
|
+
let jsInception: any
|
|
65
|
+
|
|
66
|
+
// ──────────────────────────────────────────────────────────────────
|
|
67
|
+
// PART A: Common features — SH generates, JS validates
|
|
68
|
+
// ──────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
describe('A. SH generates → JS validates', function () {
|
|
71
|
+
|
|
72
|
+
it('should create inception repo via SH (setup_git_inception_repo.sh)', async function () {
|
|
73
|
+
const result = await sh.createInceptionRepo({ repoDir: shGenRepoDir })
|
|
74
|
+
expect(result.exitCode).toBe(0)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('JS getRepoDid should read DID from SH-created repo', async function () {
|
|
78
|
+
const did = await js.getRepoDid({ repoDir: shGenRepoDir })
|
|
79
|
+
expect(did).toStartWith('did:repo:')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('JS getInceptionCommit should inspect SH-created inception commit', async function () {
|
|
83
|
+
const inception = await js.getInceptionCommit({ repoDir: shGenRepoDir })
|
|
84
|
+
expect(inception.commitHash.length).toBe(40)
|
|
85
|
+
expect(inception.did).toStartWith('did:repo:')
|
|
86
|
+
expect(inception.fullDetails).toContain('Initialize repository')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('JS auditRepository should pass on SH-created repo', async function () {
|
|
90
|
+
// Read the SH signing public key so JS can verify
|
|
91
|
+
const shPubKeyPath = join(require('os').tmpdir(), '.open-integrity-sh-signing', 'signing_key_ed25519.pub')
|
|
92
|
+
const shPubKey = (await readFile(shPubKeyPath, 'utf-8')).trim()
|
|
93
|
+
|
|
94
|
+
const audit = await js.auditRepository({
|
|
95
|
+
repoDir: shGenRepoDir,
|
|
96
|
+
allowedSigners: [{ email: 'oi-sh@test.local', publicKey: shPubKey }],
|
|
97
|
+
})
|
|
98
|
+
expect(audit.totalCommits).toBe(1)
|
|
99
|
+
expect(audit.inceptionCommitEmpty).toBe(true)
|
|
100
|
+
expect(audit.did).toStartWith('did:repo:')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('JS and SH should agree on the DID', async function () {
|
|
104
|
+
const jsDid = await js.getRepoDid({ repoDir: shGenRepoDir })
|
|
105
|
+
const shDid = await sh.getRepoDid({ repoDir: shGenRepoDir })
|
|
106
|
+
expect(jsDid).toBe(shDid)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// ──────────────────────────────────────────────────────────────────
|
|
111
|
+
// PART B: Common features — JS generates, SH validates
|
|
112
|
+
// ──────────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
describe('B. JS generates → SH validates', function () {
|
|
115
|
+
|
|
116
|
+
it('should generate a signing key via JS', async function () {
|
|
117
|
+
aliceKey = await js.generateSigningKey({
|
|
118
|
+
keyDir: keysDir,
|
|
119
|
+
keyName: 'alice_ed25519',
|
|
120
|
+
passphrase: '',
|
|
121
|
+
})
|
|
122
|
+
expect(aliceKey.publicKey).toContain('ssh-ed25519')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('should create inception repo via JS (createInceptionCommit)', async function () {
|
|
126
|
+
jsInception = await js.createInceptionCommit({
|
|
127
|
+
repoDir: jsGenRepoDir,
|
|
128
|
+
signingKeyPath: aliceKey.privateKeyPath,
|
|
129
|
+
authorName: '@Alice',
|
|
130
|
+
authorEmail: 'alice@example.com',
|
|
131
|
+
})
|
|
132
|
+
expect(jsInception.commitHash.length).toBe(40)
|
|
133
|
+
expect(jsInception.did).toStartWith('did:repo:')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('SH getRepoDid should read DID from JS-created repo', async function () {
|
|
137
|
+
const did = await sh.getRepoDid({ repoDir: jsGenRepoDir })
|
|
138
|
+
expect(did).toBe(jsInception.did)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('SH auditInceptionCommit should fail on JS-created repo (different signing key)', async function () {
|
|
142
|
+
// The SH audit uses its own signing key, not Alice's key,
|
|
143
|
+
// so it correctly fails to verify the JS-created inception commit.
|
|
144
|
+
const audit = await sh.auditInceptionCommit({
|
|
145
|
+
repoDir: jsGenRepoDir,
|
|
146
|
+
quiet: true,
|
|
147
|
+
noPrompt: true,
|
|
148
|
+
noColor: true,
|
|
149
|
+
})
|
|
150
|
+
expect(audit.passed).toBe(false)
|
|
151
|
+
expect(audit.exitCode).not.toBe(0)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('JS and SH should agree on the DID', async function () {
|
|
155
|
+
const jsDid = await js.getRepoDid({ repoDir: jsGenRepoDir })
|
|
156
|
+
const shDid = await sh.getRepoDid({ repoDir: jsGenRepoDir })
|
|
157
|
+
expect(jsDid).toBe(shDid)
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// ──────────────────────────────────────────────────────────────────
|
|
162
|
+
// PART C: JS-only features, then SH re-validates
|
|
163
|
+
// ──────────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
describe('C. JS-only features → SH re-validates', function () {
|
|
166
|
+
|
|
167
|
+
it('should generate a second signing key (Bob)', async function () {
|
|
168
|
+
bobKey = await js.generateSigningKey({
|
|
169
|
+
keyDir: keysDir,
|
|
170
|
+
keyName: 'bob_ed25519',
|
|
171
|
+
passphrase: '',
|
|
172
|
+
})
|
|
173
|
+
expect(bobKey.publicKey).toContain('ssh-ed25519')
|
|
174
|
+
expect(bobKey.fingerprint).not.toBe(aliceKey.fingerprint)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('should create inception repo for JS-only workflow', async function () {
|
|
178
|
+
const inception = await js.createInceptionCommit({
|
|
179
|
+
repoDir: jsOnlyRepoDir,
|
|
180
|
+
signingKeyPath: aliceKey.privateKeyPath,
|
|
181
|
+
authorName: '@Alice',
|
|
182
|
+
authorEmail: 'alice@example.com',
|
|
183
|
+
})
|
|
184
|
+
expect(inception.commitHash.length).toBe(40)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('should verify inception commit with allowedSigners', async function () {
|
|
188
|
+
const result = await js.verifyInceptionCommit({
|
|
189
|
+
repoDir: jsOnlyRepoDir,
|
|
190
|
+
allowedSigners: [{ email: 'alice@example.com', publicKey: aliceKey.publicKey }],
|
|
191
|
+
})
|
|
192
|
+
expect(result.valid).toBe(true)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('should allow Bob to create a signed commit', async function () {
|
|
196
|
+
const result = await js.createSignedCommit({
|
|
197
|
+
repoDir: jsOnlyRepoDir,
|
|
198
|
+
signingKeyPath: bobKey.privateKeyPath,
|
|
199
|
+
message: 'Bob adds feature',
|
|
200
|
+
authorName: '@Bob',
|
|
201
|
+
authorEmail: 'bob@example.com',
|
|
202
|
+
allowEmpty: true,
|
|
203
|
+
})
|
|
204
|
+
expect(result.commitHash).toBeDefined()
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('should verify Bob\'s commit with allowedSigners', async function () {
|
|
208
|
+
const result = await js.verifyCommit({
|
|
209
|
+
repoDir: jsOnlyRepoDir,
|
|
210
|
+
allowedSigners: [
|
|
211
|
+
{ email: 'alice@example.com', publicKey: aliceKey.publicKey },
|
|
212
|
+
{ email: 'bob@example.com', publicKey: bobKey.publicKey },
|
|
213
|
+
],
|
|
214
|
+
})
|
|
215
|
+
expect(result.valid).toBe(true)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('should list all 2 commits', async function () {
|
|
219
|
+
const result = await js.listCommits({ repoDir: jsOnlyRepoDir, reverse: true })
|
|
220
|
+
expect(result.count).toBe(2)
|
|
221
|
+
expect(result.commits[0].message).toBe('[GordianOpenIntegrity] Establish a SHA-1 root of trust for origin and future commit verification.')
|
|
222
|
+
expect(result.commits[1].message).toBe('Bob adds feature')
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('should get latest commit details', async function () {
|
|
226
|
+
const details = await js.getCommitDetails({ repoDir: jsOnlyRepoDir })
|
|
227
|
+
expect(details.message).toBe('Bob adds feature')
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('JS auditRepository should pass with allowedSigners', async function () {
|
|
231
|
+
const audit = await js.auditRepository({
|
|
232
|
+
repoDir: jsOnlyRepoDir,
|
|
233
|
+
allowedSigners: [
|
|
234
|
+
{ email: 'alice@example.com', publicKey: aliceKey.publicKey },
|
|
235
|
+
{ email: 'bob@example.com', publicKey: bobKey.publicKey },
|
|
236
|
+
],
|
|
237
|
+
})
|
|
238
|
+
expect(audit.totalCommits).toBe(2)
|
|
239
|
+
expect(audit.validSignatures).toBe(2)
|
|
240
|
+
expect(audit.invalidSignatures).toBe(0)
|
|
241
|
+
expect(audit.inceptionCommitValid).toBe(true)
|
|
242
|
+
expect(audit.inceptionCommitEmpty).toBe(true)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
// SH re-validates the repo after all JS-only mutations
|
|
246
|
+
|
|
247
|
+
it('SH getRepoDid should still work after JS-only operations', async function () {
|
|
248
|
+
const did = await sh.getRepoDid({ repoDir: jsOnlyRepoDir })
|
|
249
|
+
expect(did).toStartWith('did:repo:')
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('JS and SH should agree on the DID after all mutations', async function () {
|
|
253
|
+
const jsDid = await js.getRepoDid({ repoDir: jsOnlyRepoDir })
|
|
254
|
+
const shDid = await sh.getRepoDid({ repoDir: jsOnlyRepoDir })
|
|
255
|
+
expect(jsDid).toBe(shDid)
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
})
|