kxco-pq-chain 1.0.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 +34 -0
- package/README.md +147 -0
- package/package.json +66 -0
- package/src/client.js +170 -0
- package/src/errors.js +9 -0
- package/src/index.d.ts +96 -0
- package/src/index.js +4 -0
- package/src/intents.js +82 -0
- package/src/jcs.js +34 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction, and distribution.
|
|
10
|
+
"Licensor" shall mean KXCO by Knightsbridge.
|
|
11
|
+
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity.
|
|
12
|
+
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
|
13
|
+
"Source" form shall mean the preferred form for making modifications.
|
|
14
|
+
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form.
|
|
15
|
+
"Work" shall mean the work of authorship made available under the License.
|
|
16
|
+
"Derivative Works" shall mean any work that is based on the Work.
|
|
17
|
+
"Contribution" shall mean any work of authorship submitted to the Licensor for inclusion in the Work.
|
|
18
|
+
"Contributor" shall mean Licensor and any Legal Entity on behalf of whom a Contribution has been received by the Licensor and incorporated within the Work.
|
|
19
|
+
|
|
20
|
+
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
|
21
|
+
|
|
22
|
+
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work.
|
|
23
|
+
|
|
24
|
+
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work; and (d) If the Work includes a "NOTICE" text file, You must include a readable copy of the attribution notices contained within such NOTICE file.
|
|
25
|
+
|
|
26
|
+
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions.
|
|
27
|
+
|
|
28
|
+
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor.
|
|
29
|
+
|
|
30
|
+
7. Disclaimer of Warranty. THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
|
|
31
|
+
|
|
32
|
+
8. Limitation of Liability. IN NO EVENT SHALL ANY CONTRIBUTOR BE LIABLE FOR ANY DAMAGES ARISING FROM THIS LICENSE OR THE USE OF THE WORK.
|
|
33
|
+
|
|
34
|
+
Copyright 2026 KXCO by Knightsbridge
|
package/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# kxco-pq-chain
|
|
2
|
+
|
|
3
|
+
HTTP client for the KXCO meta-transaction relay.
|
|
4
|
+
|
|
5
|
+
Institutions sign ML-DSA-65 intents with their existing post-quantum identity. KXCO validates the signature, pays gas in ARMR, and submits the EVM transaction on Armature L1. Institutions never hold a wallet or pay gas directly — KXCO bills monthly via invoice.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install kxco-pq-chain
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import { KxcoChain } from 'kxco-pq-chain'
|
|
19
|
+
|
|
20
|
+
// identity is a KxcoIdentity from kxco-pq-sdk
|
|
21
|
+
const chain = new KxcoChain({
|
|
22
|
+
relay: 'https://relay.kxco.ai',
|
|
23
|
+
identity: institutionIdentity,
|
|
24
|
+
timeout: 10_000, // optional, ms (default 10 000)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// Register an institution on-chain (called once during onboarding)
|
|
28
|
+
const { txHash, blockNumber } = await chain.registerInstitution({
|
|
29
|
+
publicKeyHex: Buffer.from(identity.publicKey).toString('hex'),
|
|
30
|
+
metadataUrl: 'https://example.com/institution.json', // optional
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// Record a credential issuance
|
|
34
|
+
await chain.issueCredential({
|
|
35
|
+
userKid: 'aa29f37ab7f4b2cf',
|
|
36
|
+
userPublicKeyHex: Buffer.from(userPublicKey).toString('hex'),
|
|
37
|
+
role: 'verified-user',
|
|
38
|
+
expiresAt: 1800000000, // unix seconds, optional (0 = no expiry)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// Revoke a credential
|
|
42
|
+
await chain.revokeCredential({
|
|
43
|
+
userKid: 'aa29f37ab7f4b2cf',
|
|
44
|
+
reason: 'kyc-expired', // optional
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// Anchor an audit log checkpoint
|
|
48
|
+
await chain.anchorAuditRoot({
|
|
49
|
+
rootHash: 'a3f1...64-hex-chars',
|
|
50
|
+
entryCount: 100,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// Anchor an attestation envelope hash
|
|
54
|
+
await chain.anchorAttestation({
|
|
55
|
+
payloadHash: 'b7c2...64-hex-chars',
|
|
56
|
+
purpose: 'regulatory-report',
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Record a key rotation
|
|
60
|
+
await chain.rotateKey({
|
|
61
|
+
newKid: 'bb39a48bc5e4d1f0',
|
|
62
|
+
newPublicKeyHex: Buffer.from(newPublicKey).toString('hex'),
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
All methods return `Promise<{ txHash: string, blockNumber: number }>` on success and throw `KxcoChainError` on failure.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Error handling
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
import { KxcoChain, KxcoChainError } from 'kxco-pq-chain'
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
await chain.issueCredential({ ... })
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (err instanceof KxcoChainError) {
|
|
79
|
+
console.error(err.code) // 'CREDIT_EXHAUSTED', 'TIMEOUT', 'NETWORK_ERROR', ...
|
|
80
|
+
console.error(err.status) // HTTP status or null
|
|
81
|
+
console.error(err.body) // raw relay response body or null
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Common error codes: `BAD_CONFIG`, `TIMEOUT`, `NETWORK_ERROR`, `PARSE_ERROR`, `RELAY_ERROR`, plus relay-specific codes like `CREDIT_EXHAUSTED`.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Relay request format
|
|
91
|
+
|
|
92
|
+
Every request is a signed JSON intent. The relay validates the ML-DSA-65 signature against the registered institution public key before submitting the EVM transaction.
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"operation": "issueCredential",
|
|
97
|
+
"institutionKid": "aa29f37ab7f4b2cf",
|
|
98
|
+
"nonce": "<64 random hex chars>",
|
|
99
|
+
"timestamp": 1748342400,
|
|
100
|
+
"payload": { "...JCS-canonical operation fields..." },
|
|
101
|
+
"signature": "<ML-DSA-65 hex signature>"
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Signing message (newline-delimited UTF-8):
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
kxco-relay-v1
|
|
109
|
+
operation: issueCredential
|
|
110
|
+
institutionKid: aa29f37ab7f4b2cf
|
|
111
|
+
nonce: <hex>
|
|
112
|
+
timestamp: <unix seconds>
|
|
113
|
+
payload: <JCS-canonical JSON of payload>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Replay protection: the relay rejects requests where `timestamp` is outside ±5 minutes of server time, or where the `nonce` has been seen before.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Low-level helpers
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
import { buildSigningMessage, buildIntent, randomNonce, canonicalize } from 'kxco-pq-chain'
|
|
124
|
+
|
|
125
|
+
// Build the canonical signing message for a relay intent
|
|
126
|
+
const msg = buildSigningMessage(operation, institutionKid, nonce, timestamp, payload)
|
|
127
|
+
|
|
128
|
+
// Build and sign a complete relay intent object
|
|
129
|
+
const intent = await buildIntent({ operation, institutionKid, payload, identity })
|
|
130
|
+
|
|
131
|
+
// Generate a cryptographically random 64-hex-char nonce
|
|
132
|
+
const nonce = randomNonce()
|
|
133
|
+
|
|
134
|
+
// RFC 8785 JSON Canonicalization Scheme
|
|
135
|
+
const canonical = canonicalize({ b: 2, a: 1 }) // '{"a":1,"b":2}'
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Requirements
|
|
141
|
+
|
|
142
|
+
- Node.js 20.19+
|
|
143
|
+
- `kxco-post-quantum` (installed automatically as a dependency)
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
Apache-2.0 — Copyright 2026 KXCO by Knightsbridge
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kxco-pq-chain",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "HTTP client for the KXCO meta-transaction relay. Institutions sign ML-DSA-65 intents; KXCO pays gas and submits EVM transactions on Armature L1.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"post-quantum",
|
|
7
|
+
"pqc",
|
|
8
|
+
"ml-dsa",
|
|
9
|
+
"nist",
|
|
10
|
+
"fips-204",
|
|
11
|
+
"blockchain",
|
|
12
|
+
"meta-transaction",
|
|
13
|
+
"relay",
|
|
14
|
+
"kxco",
|
|
15
|
+
"armature",
|
|
16
|
+
"identity-registry",
|
|
17
|
+
"credential",
|
|
18
|
+
"audit-anchor",
|
|
19
|
+
"attestation"
|
|
20
|
+
],
|
|
21
|
+
"license": "Apache-2.0",
|
|
22
|
+
"author": "KXCO by Knightsbridge <hello@kxco.ai>",
|
|
23
|
+
"contributors": [
|
|
24
|
+
{
|
|
25
|
+
"name": "Shayne Heffernan"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "John Heffernan"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"homepage": "https://kxco.ai",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/JackKXCO/kxco-pq-chain.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/JackKXCO/kxco-pq-chain/issues"
|
|
38
|
+
},
|
|
39
|
+
"type": "module",
|
|
40
|
+
"sideEffects": false,
|
|
41
|
+
"main": "./src/index.js",
|
|
42
|
+
"types": "./src/index.d.ts",
|
|
43
|
+
"exports": {
|
|
44
|
+
".": {
|
|
45
|
+
"types": "./src/index.d.ts",
|
|
46
|
+
"import": "./src/index.js"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"src",
|
|
51
|
+
"LICENSE"
|
|
52
|
+
],
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=20.19"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"kxco-post-quantum": "^1.1.6"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"test": "node --test --test-timeout=30000 test/chain.test.js"
|
|
61
|
+
},
|
|
62
|
+
"funding": "https://kxco.ai",
|
|
63
|
+
"publishConfig": {
|
|
64
|
+
"access": "public"
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/client.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KxcoChain — HTTP client for the KXCO meta-transaction relay.
|
|
3
|
+
*
|
|
4
|
+
* Institutions never interact with Armature L1 directly. This client
|
|
5
|
+
* sends ML-DSA-65 signed intents to the KXCO relay, which validates
|
|
6
|
+
* the signature and submits the EVM transaction on the institution's behalf.
|
|
7
|
+
*
|
|
8
|
+
* All six operations return { txHash, blockNumber } on success and throw
|
|
9
|
+
* KxcoChainError on failure.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { buildIntent } from './intents.js'
|
|
13
|
+
import { KxcoChainError } from './errors.js'
|
|
14
|
+
|
|
15
|
+
export class KxcoChain {
|
|
16
|
+
#relay
|
|
17
|
+
#identity
|
|
18
|
+
#timeout
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {object} opts
|
|
22
|
+
* @param {string} opts.relay — relay base URL, e.g. 'https://relay.kxco.ai'
|
|
23
|
+
* @param {object} opts.identity — KxcoIdentity from kxco-pq-sdk (must have .kid and .sign())
|
|
24
|
+
* @param {number} [opts.timeout] — request timeout in ms (default 10000)
|
|
25
|
+
*/
|
|
26
|
+
constructor({ relay, identity, timeout = 10_000 }) {
|
|
27
|
+
if (!relay) throw new KxcoChainError('relay URL is required', { code: 'BAD_CONFIG' })
|
|
28
|
+
if (!identity) throw new KxcoChainError('identity is required', { code: 'BAD_CONFIG' })
|
|
29
|
+
this.#relay = relay.replace(/\/$/, '')
|
|
30
|
+
this.#identity = identity
|
|
31
|
+
this.#timeout = timeout
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── operations ──────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Register an institution on-chain. Called once during institution onboarding.
|
|
38
|
+
* @param {object} opts
|
|
39
|
+
* @param {string} opts.publicKeyHex — hex-encoded 1952-byte ML-DSA-65 public key
|
|
40
|
+
* @param {string} [opts.metadataUrl] — URL of institution metadata JSON
|
|
41
|
+
* @returns {Promise<{txHash: string, blockNumber: number}>}
|
|
42
|
+
*/
|
|
43
|
+
async registerInstitution({ publicKeyHex, metadataUrl = '' }) {
|
|
44
|
+
return this.#send('registerInstitution', {
|
|
45
|
+
publicKeyHex,
|
|
46
|
+
metadataUrl,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Record a user credential issuance on-chain.
|
|
52
|
+
* @param {object} opts
|
|
53
|
+
* @param {string} opts.userKid — 16-hex-char kid of the issued user
|
|
54
|
+
* @param {string} opts.userPublicKeyHex — hex-encoded user ML-DSA-65 public key
|
|
55
|
+
* @param {string} opts.role — role string (e.g. 'verified-user')
|
|
56
|
+
* @param {number} [opts.expiresAt] — unix seconds; omit or 0 = no expiry
|
|
57
|
+
* @returns {Promise<{txHash: string, blockNumber: number}>}
|
|
58
|
+
*/
|
|
59
|
+
async issueCredential({ userKid, userPublicKeyHex, role, expiresAt = 0 }) {
|
|
60
|
+
return this.#send('issueCredential', {
|
|
61
|
+
userKid,
|
|
62
|
+
userPublicKeyHex,
|
|
63
|
+
role,
|
|
64
|
+
expiresAt,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Revoke a user credential on-chain.
|
|
70
|
+
* @param {object} opts
|
|
71
|
+
* @param {string} opts.userKid — kid of the user whose credential is revoked
|
|
72
|
+
* @param {string} [opts.reason] — human-readable revocation reason
|
|
73
|
+
* @returns {Promise<{txHash: string, blockNumber: number}>}
|
|
74
|
+
*/
|
|
75
|
+
async revokeCredential({ userKid, reason = '' }) {
|
|
76
|
+
return this.#send('revokeCredential', {
|
|
77
|
+
userKid,
|
|
78
|
+
reason,
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Anchor an audit log checkpoint on-chain.
|
|
84
|
+
* @param {object} opts
|
|
85
|
+
* @param {string} opts.rootHash — hex SHA-256 of the latest AuditLog entry hash
|
|
86
|
+
* @param {number} opts.entryCount — total entries in the log at checkpoint time
|
|
87
|
+
* @returns {Promise<{txHash: string, blockNumber: number}>}
|
|
88
|
+
*/
|
|
89
|
+
async anchorAuditRoot({ rootHash, entryCount }) {
|
|
90
|
+
return this.#send('anchorAuditRoot', {
|
|
91
|
+
rootHash,
|
|
92
|
+
entryCount,
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Anchor a high-value attestation envelope hash on-chain.
|
|
98
|
+
* @param {object} opts
|
|
99
|
+
* @param {string} opts.payloadHash — hex SHA-256 of the signed attestation envelope
|
|
100
|
+
* @param {string} opts.purpose — purpose string (e.g. 'regulatory-report')
|
|
101
|
+
* @returns {Promise<{txHash: string, blockNumber: number}>}
|
|
102
|
+
*/
|
|
103
|
+
async anchorAttestation({ payloadHash, purpose }) {
|
|
104
|
+
return this.#send('anchorAttestation', {
|
|
105
|
+
payloadHash,
|
|
106
|
+
purpose,
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Record an institution key rotation on-chain.
|
|
112
|
+
* @param {object} opts
|
|
113
|
+
* @param {string} opts.newKid — new 16-hex-char kid after rotation
|
|
114
|
+
* @param {string} opts.newPublicKeyHex — hex-encoded new ML-DSA-65 public key
|
|
115
|
+
* @returns {Promise<{txHash: string, blockNumber: number}>}
|
|
116
|
+
*/
|
|
117
|
+
async rotateKey({ newKid, newPublicKeyHex }) {
|
|
118
|
+
return this.#send('rotateKey', {
|
|
119
|
+
newKid,
|
|
120
|
+
newPublicKeyHex,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── internal ────────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
async #send(operation, payload) {
|
|
127
|
+
const intent = await buildIntent({
|
|
128
|
+
operation,
|
|
129
|
+
institutionKid: this.#identity.kid,
|
|
130
|
+
payload,
|
|
131
|
+
identity: this.#identity,
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
const ac = new AbortController()
|
|
135
|
+
const tid = setTimeout(() => ac.abort(), this.#timeout)
|
|
136
|
+
|
|
137
|
+
let response
|
|
138
|
+
try {
|
|
139
|
+
response = await fetch(`${this.#relay}/intents`, {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
headers: { 'content-type': 'application/json' },
|
|
142
|
+
body: JSON.stringify(intent),
|
|
143
|
+
signal: ac.signal,
|
|
144
|
+
})
|
|
145
|
+
} catch (err) {
|
|
146
|
+
clearTimeout(tid)
|
|
147
|
+
if (err.name === 'AbortError') {
|
|
148
|
+
throw new KxcoChainError(`relay request timed out after ${this.#timeout}ms`, { code: 'TIMEOUT' })
|
|
149
|
+
}
|
|
150
|
+
throw new KxcoChainError(`relay request failed: ${err.message}`, { code: 'NETWORK_ERROR' })
|
|
151
|
+
}
|
|
152
|
+
clearTimeout(tid)
|
|
153
|
+
|
|
154
|
+
let body
|
|
155
|
+
try {
|
|
156
|
+
body = await response.json()
|
|
157
|
+
} catch {
|
|
158
|
+
throw new KxcoChainError('relay returned non-JSON response', { code: 'PARSE_ERROR', status: response.status })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!response.ok || body.ok === false) {
|
|
162
|
+
throw new KxcoChainError(
|
|
163
|
+
body.error ?? `relay error ${response.status}`,
|
|
164
|
+
{ code: body.code ?? 'RELAY_ERROR', status: response.status, body }
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { txHash: body.txHash, blockNumber: body.blockNumber }
|
|
169
|
+
}
|
|
170
|
+
}
|
package/src/errors.js
ADDED
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// ── KxcoChainError ────────────────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
export class KxcoChainError extends Error {
|
|
4
|
+
name: 'KxcoChainError'
|
|
5
|
+
code: string
|
|
6
|
+
status: number | null
|
|
7
|
+
body: unknown | null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// ── Intent types ──────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export interface RelayIntent {
|
|
13
|
+
operation: string
|
|
14
|
+
institutionKid: string
|
|
15
|
+
nonce: string
|
|
16
|
+
timestamp: number
|
|
17
|
+
payload: Record<string, unknown>
|
|
18
|
+
signature: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RelayResult {
|
|
22
|
+
txHash: string
|
|
23
|
+
blockNumber: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ── KxcoChain ─────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export interface KxcoChainOptions {
|
|
29
|
+
relay: string
|
|
30
|
+
/** KxcoIdentity from kxco-pq-sdk — must have .kid (string) and .sign(Uint8Array) */
|
|
31
|
+
identity: { kid: string; sign(message: Uint8Array): Promise<Uint8Array> }
|
|
32
|
+
timeout?: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface RegisterInstitutionOpts {
|
|
36
|
+
publicKeyHex: string
|
|
37
|
+
metadataUrl?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface IssueCredentialOpts {
|
|
41
|
+
userKid: string
|
|
42
|
+
userPublicKeyHex: string
|
|
43
|
+
role: string
|
|
44
|
+
expiresAt?: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RevokeCredentialOpts {
|
|
48
|
+
userKid: string
|
|
49
|
+
reason?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AnchorAuditRootOpts {
|
|
53
|
+
rootHash: string
|
|
54
|
+
entryCount: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AnchorAttestationOpts {
|
|
58
|
+
payloadHash: string
|
|
59
|
+
purpose: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface RotateKeyOpts {
|
|
63
|
+
newKid: string
|
|
64
|
+
newPublicKeyHex: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class KxcoChain {
|
|
68
|
+
constructor(opts: KxcoChainOptions)
|
|
69
|
+
registerInstitution(opts: RegisterInstitutionOpts): Promise<RelayResult>
|
|
70
|
+
issueCredential(opts: IssueCredentialOpts): Promise<RelayResult>
|
|
71
|
+
revokeCredential(opts: RevokeCredentialOpts): Promise<RelayResult>
|
|
72
|
+
anchorAuditRoot(opts: AnchorAuditRootOpts): Promise<RelayResult>
|
|
73
|
+
anchorAttestation(opts: AnchorAttestationOpts): Promise<RelayResult>
|
|
74
|
+
rotateKey(opts: RotateKeyOpts): Promise<RelayResult>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Low-level helpers ─────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
export function buildSigningMessage(
|
|
80
|
+
operation: string,
|
|
81
|
+
institutionKid: string,
|
|
82
|
+
nonce: string,
|
|
83
|
+
timestamp: number,
|
|
84
|
+
payload: Record<string, unknown>,
|
|
85
|
+
): Uint8Array
|
|
86
|
+
|
|
87
|
+
export function randomNonce(): string
|
|
88
|
+
|
|
89
|
+
export function buildIntent(opts: {
|
|
90
|
+
operation: string
|
|
91
|
+
institutionKid: string
|
|
92
|
+
payload: Record<string, unknown>
|
|
93
|
+
identity: { kid: string; sign(message: Uint8Array): Promise<Uint8Array> }
|
|
94
|
+
}): Promise<RelayIntent>
|
|
95
|
+
|
|
96
|
+
export function canonicalize(value: unknown): string
|
package/src/index.js
ADDED
package/src/intents.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relay intent builder + signer.
|
|
3
|
+
*
|
|
4
|
+
* Every relay request is a signed JSON intent that proves the institution
|
|
5
|
+
* authorised the operation. The relay validates the ML-DSA-65 signature
|
|
6
|
+
* off-chain before submitting the EVM transaction.
|
|
7
|
+
*
|
|
8
|
+
* Signing message format (newline-delimited, UTF-8):
|
|
9
|
+
*
|
|
10
|
+
* kxco-relay-v1
|
|
11
|
+
* operation: <name>
|
|
12
|
+
* institutionKid: <16-hex-char kid>
|
|
13
|
+
* nonce: <64 random hex chars>
|
|
14
|
+
* timestamp: <unix seconds>
|
|
15
|
+
* payload: <JCS-canonical JSON of the payload object>
|
|
16
|
+
*
|
|
17
|
+
* Replay protection: the relay rejects requests where timestamp is outside
|
|
18
|
+
* ±5 minutes of server time, or where the nonce has been seen before.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { canonicalize } from './jcs.js'
|
|
22
|
+
|
|
23
|
+
const enc = new TextEncoder()
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build the signing message for a relay intent.
|
|
27
|
+
* @param {string} operation
|
|
28
|
+
* @param {string} institutionKid
|
|
29
|
+
* @param {string} nonce — 64 random hex chars
|
|
30
|
+
* @param {number} timestamp — unix seconds
|
|
31
|
+
* @param {object} payload
|
|
32
|
+
* @returns {Uint8Array}
|
|
33
|
+
*/
|
|
34
|
+
export function buildSigningMessage(operation, institutionKid, nonce, timestamp, payload) {
|
|
35
|
+
return enc.encode([
|
|
36
|
+
'kxco-relay-v1',
|
|
37
|
+
`operation: ${operation}`,
|
|
38
|
+
`institutionKid: ${institutionKid}`,
|
|
39
|
+
`nonce: ${nonce}`,
|
|
40
|
+
`timestamp: ${timestamp}`,
|
|
41
|
+
`payload: ${canonicalize(payload)}`,
|
|
42
|
+
].join('\n'))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Generate a cryptographically random 64-hex-char nonce.
|
|
47
|
+
* Node 20+ exposes globalThis.crypto; earlier versions use node:crypto.
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
export function randomNonce() {
|
|
51
|
+
const bytes = new Uint8Array(32)
|
|
52
|
+
// globalThis.crypto is available in Node 20+ and all modern browsers
|
|
53
|
+
globalThis.crypto.getRandomValues(bytes)
|
|
54
|
+
return Buffer.from(bytes).toString('hex')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build and sign a relay intent payload.
|
|
59
|
+
*
|
|
60
|
+
* @param {object} opts
|
|
61
|
+
* @param {string} opts.operation
|
|
62
|
+
* @param {string} opts.institutionKid
|
|
63
|
+
* @param {object} opts.payload
|
|
64
|
+
* @param {object} opts.identity — KxcoIdentity (must have .kid and .sign())
|
|
65
|
+
* @returns {Promise<object>} — the complete signed intent object
|
|
66
|
+
*/
|
|
67
|
+
export async function buildIntent({ operation, institutionKid, payload, identity }) {
|
|
68
|
+
const nonce = randomNonce()
|
|
69
|
+
const timestamp = Math.floor(Date.now() / 1000)
|
|
70
|
+
const message = buildSigningMessage(operation, institutionKid, nonce, timestamp, payload)
|
|
71
|
+
const sigBytes = await identity.sign(message)
|
|
72
|
+
const signature = Buffer.from(sigBytes).toString('hex')
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
operation,
|
|
76
|
+
institutionKid,
|
|
77
|
+
nonce,
|
|
78
|
+
timestamp,
|
|
79
|
+
payload,
|
|
80
|
+
signature,
|
|
81
|
+
}
|
|
82
|
+
}
|
package/src/jcs.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// RFC 8785 JSON Canonicalization Scheme (JCS), subset.
|
|
2
|
+
// Copied from kxco-pq-cli/src/jcs.js — do not diverge.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Canonicalize a JSON-serializable value per (a subset of) RFC 8785.
|
|
6
|
+
* @param {unknown} value
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
export function canonicalize(value) {
|
|
10
|
+
return JSON.stringify(walk(value))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function walk(v) {
|
|
14
|
+
if (v === null) return null
|
|
15
|
+
if (typeof v === 'boolean') return v
|
|
16
|
+
if (typeof v === 'string') return v
|
|
17
|
+
if (typeof v === 'number') {
|
|
18
|
+
if (!Number.isFinite(v)) throw new TypeError('JCS: non-finite numbers are not representable in JSON')
|
|
19
|
+
if (!Number.isInteger(v)) throw new TypeError('JCS subset: floats are not supported')
|
|
20
|
+
return v
|
|
21
|
+
}
|
|
22
|
+
if (Array.isArray(v)) return v.map(walk)
|
|
23
|
+
if (v && typeof v === 'object') {
|
|
24
|
+
const out = {}
|
|
25
|
+
for (const k of Object.keys(v).sort()) {
|
|
26
|
+
const child = walk(v[k])
|
|
27
|
+
if (child === undefined) continue
|
|
28
|
+
out[k] = child
|
|
29
|
+
}
|
|
30
|
+
return out
|
|
31
|
+
}
|
|
32
|
+
if (v === undefined) return undefined
|
|
33
|
+
throw new TypeError(`JCS: unsupported value of type ${typeof v}`)
|
|
34
|
+
}
|