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 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
@@ -0,0 +1,9 @@
1
+ export class KxcoChainError extends Error {
2
+ constructor(message, { code, status, body } = {}) {
3
+ super(message)
4
+ this.name = 'KxcoChainError'
5
+ this.code = code ?? 'CHAIN_ERROR'
6
+ this.status = status ?? null
7
+ this.body = body ?? null
8
+ }
9
+ }
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
@@ -0,0 +1,4 @@
1
+ export { KxcoChain } from './client.js'
2
+ export { KxcoChainError } from './errors.js'
3
+ export { buildIntent, buildSigningMessage, randomNonce } from './intents.js'
4
+ export { canonicalize } from './jcs.js'
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
+ }