@rodit/verify-hola 0.1.3
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/README.md +93 -0
- package/bin/verify-hola.js +103 -0
- package/lib/hola-validate.js +178 -0
- package/lib/nonce-encoding.js +26 -0
- package/lib/parse-dn.js +89 -0
- package/lib/report.js +133 -0
- package/lib/verify-rpc.js +250 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# @rodit/verify-hola
|
|
2
|
+
|
|
3
|
+
Part of the [Discernible IO SDK monorepo](../README.md).
|
|
4
|
+
|
|
5
|
+
Verify-before-execute tooling for IdentyClaw HOLA lines.
|
|
6
|
+
|
|
7
|
+
For NEAR account enrollment, see [gennearaccount](https://github.com/discernible-io/gennearaccount). For in-gateway HOLA tools, see [openclaw-identyclaw-plugin](https://github.com/discernible-io/openclaw-identyclaw-plugin).
|
|
8
|
+
|
|
9
|
+
## Recommended: direct NEAR RPC (`--rpc`)
|
|
10
|
+
|
|
11
|
+
No IdentyClaw API in the verify path — your machine reads chain state via FastNear (or any NEAR RPC).
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
export NEAR_RPC_URL="https://rpc.mainnet.fastnear.com"
|
|
15
|
+
export NEAR_CONTRACT_ID="genaaaa-identyclaw-com.near"
|
|
16
|
+
|
|
17
|
+
npx @rodit/verify-hola report "HOLA/MUNDO/..." --rpc \
|
|
18
|
+
--canonical-peer <lemuel-gulliver-lobby-tokenId>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
From this monorepo (no npm publish required):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
export NEAR_RPC_URL="https://rpc.mainnet.fastnear.com"
|
|
25
|
+
export NEAR_CONTRACT_ID="genaaaa-identyclaw-com.near"
|
|
26
|
+
node bin/verify-hola.js report "HOLA/..." --rpc \
|
|
27
|
+
--canonical-peer <lemuel-gulliver-lobby-tokenId>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Alternative: IdentyClaw API path
|
|
31
|
+
|
|
32
|
+
JWT is optional — `POST /api/identity/verify` is public.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx @rodit/verify-hola report "HOLA/MUNDO/..."
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Commands
|
|
39
|
+
|
|
40
|
+
| Command | Description |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| `verify-hola verify "HOLA/..." --rpc` | Cryptographic outcome via NEAR RPC |
|
|
43
|
+
| `verify-hola report "HOLA/..." --rpc` | Verify + on-chain passport profile (DN) |
|
|
44
|
+
| `verify-hola report "HOLA/..."` | Verify + profile via API (`includeProfile`) |
|
|
45
|
+
|
|
46
|
+
## Environment
|
|
47
|
+
|
|
48
|
+
| Variable | `--rpc` | API (default) |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| `NEAR_RPC_URL` | **Required** | — |
|
|
51
|
+
| `NEAR_CONTRACT_ID` | **Required** | — |
|
|
52
|
+
| `IDENTYCLAW_BASE_URL` | — | default `https://api.identyclaw.com` |
|
|
53
|
+
| `IDENTYCLAW_JWT` | — | optional |
|
|
54
|
+
| `IDENTYCLAW_CANONICAL_PEER_ID` | optional impersonation guard | optional |
|
|
55
|
+
| `IDENTYCLAW_EXPECTED_RECIPIENT` | — | optional |
|
|
56
|
+
|
|
57
|
+
## Concierge demo (Lemuel Gulliver)
|
|
58
|
+
|
|
59
|
+
1. Message **Lemuel Gulliver** via A2A at [identyclaw-concierge.identyclaw.com:7443](https://identyclaw-concierge.identyclaw.com:7443) (or email / Discord / Telegram) and ask for a HOLA.
|
|
60
|
+
2. Verify with RPC (recommended):
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
export NEAR_RPC_URL="https://rpc.mainnet.fastnear.com"
|
|
64
|
+
export NEAR_CONTRACT_ID="genaaaa-identyclaw-com.near"
|
|
65
|
+
export IDENTYCLAW_CANONICAL_PEER_ID="<lobby-tokenId-from-agent-card>"
|
|
66
|
+
npx @rodit/verify-hola report "HOLA/..." --rpc
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Registry: [com.identyclaw.lemuel_gulliver](https://www.a2a-registry.org/agent/com.identyclaw.lemuel_gulliver)
|
|
70
|
+
|
|
71
|
+
Web UI (API path): https://verify.identyclaw.com
|
|
72
|
+
|
|
73
|
+
## API
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
const { buildHolaReportViaRpc } = require("@rodit/verify-hola/lib/verify-rpc");
|
|
77
|
+
|
|
78
|
+
const report = await buildHolaReportViaRpc(hola, {
|
|
79
|
+
nearRpcUrl: process.env.NEAR_RPC_URL,
|
|
80
|
+
nearContractId: process.env.NEAR_CONTRACT_ID,
|
|
81
|
+
canonicalPeerId: "abcdefghijkl"
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Related
|
|
86
|
+
|
|
87
|
+
- [`references/verify-hola-recipes.md`](../references/verify-hola-recipes.md)
|
|
88
|
+
- [`references/identity-verification-policy.md`](../references/identity-verification-policy.md) §1
|
|
89
|
+
- MCP `doc:reference:verify-hola-recipes`
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
[discernible.io](https://www.discernible.io) · [identyclaw-agents](https://github.com/discernible-io/identyclaw-agents) · [discernible-io](https://github.com/discernible-io)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { buildHolaReport, verifyHola } = require("../lib/report");
|
|
5
|
+
const { buildHolaReportViaRpc, verifyHolaViaRpc } = require("../lib/verify-rpc");
|
|
6
|
+
|
|
7
|
+
function usage() {
|
|
8
|
+
process.stderr.write(`Usage:
|
|
9
|
+
verify-hola verify <hola-string> [--rpc] [--expected-recipient <tokenId>]
|
|
10
|
+
verify-hola report <hola-string> [--rpc] [--expected-recipient <tokenId>] [--canonical-peer <tokenId>]
|
|
11
|
+
echo "HOLA/..." | verify-hola report - --rpc
|
|
12
|
+
|
|
13
|
+
Verify paths:
|
|
14
|
+
--rpc Direct NEAR RPC via @rodit/rodit-auth-be (recommended — no IdentyClaw API)
|
|
15
|
+
default POST /api/identity/verify on IDENTYCLAW_BASE_URL
|
|
16
|
+
|
|
17
|
+
RPC env (required with --rpc):
|
|
18
|
+
NEAR_RPC_URL e.g. https://rpc.mainnet.fastnear.com
|
|
19
|
+
NEAR_CONTRACT_ID e.g. genaaaa-identyclaw-com.near
|
|
20
|
+
NEAR_RPC_TIMEOUT optional milliseconds (default from rodit-auth-be)
|
|
21
|
+
|
|
22
|
+
API env (default path):
|
|
23
|
+
IDENTYCLAW_BASE_URL default https://api.identyclaw.com
|
|
24
|
+
IDENTYCLAW_JWT optional
|
|
25
|
+
IDENTYCLAW_CANONICAL_PEER_ID
|
|
26
|
+
IDENTYCLAW_EXPECTED_RECIPIENT
|
|
27
|
+
`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseArgs(argv) {
|
|
31
|
+
const args = [...argv];
|
|
32
|
+
const command = args.shift();
|
|
33
|
+
let hola = args[0];
|
|
34
|
+
const options = {
|
|
35
|
+
useRpc: false,
|
|
36
|
+
expectedRecipient: process.env.IDENTYCLAW_EXPECTED_RECIPIENT,
|
|
37
|
+
canonicalPeerId: process.env.IDENTYCLAW_CANONICAL_PEER_ID,
|
|
38
|
+
baseUrl: process.env.IDENTYCLAW_BASE_URL,
|
|
39
|
+
jwt: process.env.IDENTYCLAW_JWT,
|
|
40
|
+
nearRpcUrl: process.env.NEAR_RPC_URL,
|
|
41
|
+
nearContractId: process.env.NEAR_CONTRACT_ID
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (hola === "-") hola = null;
|
|
45
|
+
|
|
46
|
+
for (let i = 1; i < args.length; i += 1) {
|
|
47
|
+
const flag = args[i];
|
|
48
|
+
if (flag === "--rpc") {
|
|
49
|
+
options.useRpc = true;
|
|
50
|
+
} else if (flag === "--expected-recipient") {
|
|
51
|
+
options.expectedRecipient = args[++i];
|
|
52
|
+
} else if (flag === "--canonical-peer") {
|
|
53
|
+
options.canonicalPeerId = args[++i];
|
|
54
|
+
} else if (flag === "--base-url") {
|
|
55
|
+
options.baseUrl = args[++i];
|
|
56
|
+
} else if (flag === "--near-rpc-url") {
|
|
57
|
+
options.nearRpcUrl = args[++i];
|
|
58
|
+
} else if (flag === "--near-contract-id") {
|
|
59
|
+
options.nearContractId = args[++i];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { command, hola, options };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function readHolaFromStdin() {
|
|
67
|
+
const chunks = [];
|
|
68
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
69
|
+
const hola = Buffer.concat(chunks).toString("utf8").trim();
|
|
70
|
+
if (!hola) throw new Error("empty HOLA on stdin");
|
|
71
|
+
return hola;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function main() {
|
|
75
|
+
const { command, hola: holaArg, options } = parseArgs(process.argv.slice(2));
|
|
76
|
+
if (!command || !["verify", "report"].includes(command)) {
|
|
77
|
+
usage();
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const hola = holaArg ? holaArg : await readHolaFromStdin();
|
|
82
|
+
|
|
83
|
+
if (command === "verify") {
|
|
84
|
+
const result = options.useRpc
|
|
85
|
+
? await verifyHolaViaRpc(hola, options)
|
|
86
|
+
: await verifyHola(hola, options);
|
|
87
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
88
|
+
process.exit(result.verified ? 0 : 1);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const report = options.useRpc
|
|
93
|
+
? await buildHolaReportViaRpc(hola, options)
|
|
94
|
+
: await buildHolaReport(hola, { ...options, includeProfile: true });
|
|
95
|
+
|
|
96
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
97
|
+
process.exit(report.verified ? 0 : 1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
main().catch((err) => {
|
|
101
|
+
process.stderr.write(JSON.stringify({ verified: false, error: err.message }) + "\n");
|
|
102
|
+
process.exit(1);
|
|
103
|
+
});
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
const { validateHexNonce, computeHolaChecksum } = require("./nonce-encoding");
|
|
2
|
+
|
|
3
|
+
function validateTokenIdFormat(value) {
|
|
4
|
+
return /^[A-Z]{12}$/.test(String(value || ""));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function validateHolaMessage(hola) {
|
|
8
|
+
const result = {
|
|
9
|
+
valid: false,
|
|
10
|
+
isSubagentFormat: false,
|
|
11
|
+
recipient: "MUNDO",
|
|
12
|
+
tokenId: null,
|
|
13
|
+
delegateId: null,
|
|
14
|
+
issuerTokenId: null,
|
|
15
|
+
subagentPublicKey: null,
|
|
16
|
+
isoTimestamp: null,
|
|
17
|
+
noncetsHex: null,
|
|
18
|
+
signatureB32: null,
|
|
19
|
+
checksumChar: null,
|
|
20
|
+
reason: null,
|
|
21
|
+
checks: {
|
|
22
|
+
formatValid: false,
|
|
23
|
+
checksumValid: false,
|
|
24
|
+
timestampValid: false,
|
|
25
|
+
noncetsValid: false
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
if (!hola || typeof hola !== "string") {
|
|
30
|
+
result.reason = "Missing or invalid hola parameter (must be a string)";
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const MAX_HOLA_LENGTH = 512;
|
|
35
|
+
if (hola.length > MAX_HOLA_LENGTH) {
|
|
36
|
+
result.reason = `Hola string exceeds maximum length of ${MAX_HOLA_LENGTH} characters`;
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const prefixLiteral = "HOLA/";
|
|
41
|
+
const normalizedHola = hola.toUpperCase();
|
|
42
|
+
if (!normalizedHola.startsWith(prefixLiteral)) {
|
|
43
|
+
result.reason = "Unsupported protocol; expected HOLA prefix";
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
result.checks.formatValid = true;
|
|
48
|
+
const withoutPrefix = normalizedHola.slice(prefixLiteral.length);
|
|
49
|
+
const recipientSeparatorIndex = withoutPrefix.indexOf("/");
|
|
50
|
+
if (recipientSeparatorIndex === -1) {
|
|
51
|
+
result.reason = "Invalid HOLA format: missing destinatary separator";
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const recipientRaw = withoutPrefix.slice(0, recipientSeparatorIndex);
|
|
56
|
+
result.recipient = recipientRaw.length > 0 ? recipientRaw : "MUNDO";
|
|
57
|
+
const afterRecipient = withoutPrefix.slice(recipientSeparatorIndex + 1);
|
|
58
|
+
|
|
59
|
+
const lastSeparatorIndex = afterRecipient.lastIndexOf("/");
|
|
60
|
+
if (lastSeparatorIndex === -1) {
|
|
61
|
+
result.reason = "Invalid HOLA format: missing checksum separator";
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const checksumCharRaw = afterRecipient.slice(lastSeparatorIndex + 1);
|
|
66
|
+
const beforeChecksum = afterRecipient.slice(0, lastSeparatorIndex);
|
|
67
|
+
if (!checksumCharRaw) {
|
|
68
|
+
result.reason = "Missing checksum";
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
result.checksumChar = checksumCharRaw.toUpperCase();
|
|
72
|
+
|
|
73
|
+
const signatureSeparatorIndex = beforeChecksum.lastIndexOf("/");
|
|
74
|
+
if (signatureSeparatorIndex === -1) {
|
|
75
|
+
result.reason = "Invalid HOLA format: missing signature separator";
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
result.signatureB32 = beforeChecksum.slice(signatureSeparatorIndex + 1);
|
|
80
|
+
const beforeSignature = beforeChecksum.slice(0, signatureSeparatorIndex);
|
|
81
|
+
if (!result.signatureB32) {
|
|
82
|
+
result.reason = "Missing signature";
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const protocolSeparatorIndex = beforeSignature.lastIndexOf("/");
|
|
87
|
+
if (protocolSeparatorIndex === -1) {
|
|
88
|
+
result.reason = "Invalid HOLA format: missing protocol marker separator";
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const protocolMarker = beforeSignature.slice(protocolSeparatorIndex + 1);
|
|
93
|
+
if (protocolMarker !== "API.IDENTYCLAW.COM") {
|
|
94
|
+
result.reason = `Invalid protocol marker: expected API.IDENTYCLAW.COM, got ${protocolMarker}`;
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const beforeProtocol = beforeSignature.slice(0, protocolSeparatorIndex);
|
|
99
|
+
const noncetsHexSeparatorIndex = beforeProtocol.lastIndexOf("/");
|
|
100
|
+
if (noncetsHexSeparatorIndex === -1) {
|
|
101
|
+
result.reason = "Invalid HOLA format: missing noncets separator";
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const noncetsHexRaw = beforeProtocol.slice(noncetsHexSeparatorIndex + 1);
|
|
106
|
+
const tokenAndTimestamp = beforeProtocol.slice(0, noncetsHexSeparatorIndex);
|
|
107
|
+
if (!noncetsHexRaw) {
|
|
108
|
+
result.reason = "Missing noncets";
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const noncetsCheck = validateHexNonce(noncetsHexRaw);
|
|
113
|
+
if (!noncetsCheck.valid) {
|
|
114
|
+
result.reason = noncetsCheck.reason;
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
result.noncetsHex = noncetsCheck.original ?? noncetsCheck.value;
|
|
119
|
+
result.checks.noncetsValid = true;
|
|
120
|
+
|
|
121
|
+
const senderFields = tokenAndTimestamp.split("/");
|
|
122
|
+
if (senderFields.length === 2) {
|
|
123
|
+
result.tokenId = senderFields[0];
|
|
124
|
+
result.isoTimestamp = senderFields[1];
|
|
125
|
+
} else if (senderFields.length === 4) {
|
|
126
|
+
result.isSubagentFormat = true;
|
|
127
|
+
result.delegateId = senderFields[0];
|
|
128
|
+
result.issuerTokenId = senderFields[1];
|
|
129
|
+
result.subagentPublicKey = senderFields[2];
|
|
130
|
+
result.isoTimestamp = senderFields[3];
|
|
131
|
+
result.tokenId = result.issuerTokenId;
|
|
132
|
+
} else {
|
|
133
|
+
result.reason = "Invalid HOLA format: expected standard or subagent sender fields";
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!result.tokenId) {
|
|
138
|
+
result.reason = result.isSubagentFormat ? "Missing issuerTokenId" : "Missing tokenId";
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!validateTokenIdFormat(result.tokenId)) {
|
|
143
|
+
result.reason = result.isSubagentFormat
|
|
144
|
+
? "Invalid issuerTokenId: must be exactly 12 letters"
|
|
145
|
+
: "Invalid tokenId: must be exactly 12 letters";
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!result.isoTimestamp) {
|
|
150
|
+
result.reason = "Missing timestamp";
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const tsMs = Date.parse(result.isoTimestamp);
|
|
155
|
+
if (Number.isNaN(tsMs)) {
|
|
156
|
+
result.reason = "Invalid timestamp: must be a valid ISO-8601 string";
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
result.checks.timestampValid = true;
|
|
160
|
+
|
|
161
|
+
const signedMessage = result.isSubagentFormat
|
|
162
|
+
? `HOLA/${result.recipient}/${result.delegateId}/${result.issuerTokenId}/${result.subagentPublicKey}/${result.isoTimestamp}/${result.noncetsHex}/API.IDENTYCLAW.COM/`
|
|
163
|
+
: `HOLA/${result.recipient}/${result.tokenId}/${result.isoTimestamp}/${result.noncetsHex}/API.IDENTYCLAW.COM/`;
|
|
164
|
+
const canonicalSignedMessage = signedMessage.toUpperCase();
|
|
165
|
+
const checksumPrefix = `${canonicalSignedMessage}${result.signatureB32}/`;
|
|
166
|
+
const expectedChecksum = computeHolaChecksum(checksumPrefix);
|
|
167
|
+
|
|
168
|
+
if (result.checksumChar !== expectedChecksum) {
|
|
169
|
+
result.reason = `Invalid checksum: expected ${expectedChecksum}, got ${result.checksumChar}`;
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
result.checks.checksumValid = true;
|
|
174
|
+
result.valid = true;
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = { validateHolaMessage };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const HOLA_CHECKSUM_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ";
|
|
2
|
+
const HOLA_CHECKSUM_MOD = HOLA_CHECKSUM_ALPHABET.length;
|
|
3
|
+
|
|
4
|
+
function validateHexNonce(nonce) {
|
|
5
|
+
if (typeof nonce !== "string") {
|
|
6
|
+
return { valid: false, value: null, reason: "nonce must be a string" };
|
|
7
|
+
}
|
|
8
|
+
const upper = nonce.toUpperCase();
|
|
9
|
+
if (upper.length === 0) {
|
|
10
|
+
return { valid: false, value: null, reason: "nonce must not be empty" };
|
|
11
|
+
}
|
|
12
|
+
if (!/^[0-9A-F]+$/.test(upper)) {
|
|
13
|
+
return { valid: false, value: null, reason: "nonce must contain only [0-9A-F] characters" };
|
|
14
|
+
}
|
|
15
|
+
return { valid: true, value: upper, original: nonce, reason: null };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function computeHolaChecksum(prefix) {
|
|
19
|
+
let sum = 0;
|
|
20
|
+
for (let i = 0; i < prefix.length; i += 1) {
|
|
21
|
+
sum += prefix.charCodeAt(i);
|
|
22
|
+
}
|
|
23
|
+
return HOLA_CHECKSUM_ALPHABET[sum % HOLA_CHECKSUM_MOD];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { validateHexNonce, computeHolaChecksum };
|
package/lib/parse-dn.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse userselected_dn comma-separated key=value pairs (public subset for verify report).
|
|
3
|
+
*/
|
|
4
|
+
function splitDistinguishedName(rawDn) {
|
|
5
|
+
const parts = [];
|
|
6
|
+
let current = "";
|
|
7
|
+
let inQuotes = false;
|
|
8
|
+
|
|
9
|
+
for (let i = 0; i < rawDn.length; i += 1) {
|
|
10
|
+
const ch = rawDn[i];
|
|
11
|
+
if (ch === '"') {
|
|
12
|
+
inQuotes = !inQuotes;
|
|
13
|
+
current += ch;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (ch === "," && !inQuotes) {
|
|
17
|
+
if (current.trim()) parts.push(current.trim());
|
|
18
|
+
current = "";
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
current += ch;
|
|
22
|
+
}
|
|
23
|
+
if (current.trim()) parts.push(current.trim());
|
|
24
|
+
return parts;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseDnComponent(part) {
|
|
28
|
+
const eq = part.indexOf("=");
|
|
29
|
+
if (eq <= 0) return null;
|
|
30
|
+
const attribute = part.slice(0, eq).trim();
|
|
31
|
+
let value = part.slice(eq + 1).trim();
|
|
32
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
33
|
+
value = value.slice(1, -1);
|
|
34
|
+
}
|
|
35
|
+
return { attribute, value };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseUserSelectedDn(rawDn) {
|
|
39
|
+
if (typeof rawDn !== "string" || rawDn.trim().length === 0) {
|
|
40
|
+
return {
|
|
41
|
+
displayName: null,
|
|
42
|
+
creature: null,
|
|
43
|
+
contactUris: [],
|
|
44
|
+
webhookUrl: null,
|
|
45
|
+
raw: rawDn || null
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const components = splitDistinguishedName(rawDn.trim())
|
|
50
|
+
.map(parseDnComponent)
|
|
51
|
+
.filter(Boolean);
|
|
52
|
+
|
|
53
|
+
const attributes = {};
|
|
54
|
+
for (const component of components) {
|
|
55
|
+
const key = component.attribute;
|
|
56
|
+
if (Object.prototype.hasOwnProperty.call(attributes, key)) {
|
|
57
|
+
const existing = attributes[key];
|
|
58
|
+
attributes[key] = Array.isArray(existing)
|
|
59
|
+
? [...existing, component.value]
|
|
60
|
+
: [existing, component.value];
|
|
61
|
+
} else {
|
|
62
|
+
attributes[key] = component.value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const find = (name) =>
|
|
67
|
+
components.find((c) => c.attribute.toUpperCase() === name.toUpperCase()) || null;
|
|
68
|
+
|
|
69
|
+
const nnswf = find("NNSWF");
|
|
70
|
+
const nswf = find("NSWF");
|
|
71
|
+
const displayNameParts = [];
|
|
72
|
+
if (nnswf?.value) displayNameParts.push(nnswf.value);
|
|
73
|
+
if (nswf?.value) displayNameParts.push(nswf.value);
|
|
74
|
+
|
|
75
|
+
const contactUris = components
|
|
76
|
+
.filter((c) => c.attribute.toUpperCase() === "CONTACTURI")
|
|
77
|
+
.map((c) => c.value);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
displayName: displayNameParts.length > 0 ? displayNameParts.join(" ").trim() : null,
|
|
81
|
+
creature: attributes.Creature || attributes.creature || null,
|
|
82
|
+
contactUris,
|
|
83
|
+
webhookUrl: attributes.webhook_url || null,
|
|
84
|
+
raw: rawDn.trim(),
|
|
85
|
+
attributes
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { parseUserSelectedDn };
|
package/lib/report.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
const { parseUserSelectedDn } = require("./parse-dn");
|
|
2
|
+
|
|
3
|
+
const DEFAULT_BASE = "https://api.identyclaw.com";
|
|
4
|
+
|
|
5
|
+
async function verifyHola(hola, { baseUrl = DEFAULT_BASE, jwt, expectedRecipient, includeProfile = false } = {}) {
|
|
6
|
+
const body = { hola };
|
|
7
|
+
if (expectedRecipient) body.expectedRecipient = expectedRecipient;
|
|
8
|
+
if (includeProfile) body.includeProfile = true;
|
|
9
|
+
|
|
10
|
+
const headers = { "Content-Type": "application/json", Accept: "application/json" };
|
|
11
|
+
if (jwt) headers.Authorization = `Bearer ${jwt}`;
|
|
12
|
+
|
|
13
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/api/identity/verify`, {
|
|
14
|
+
method: "POST",
|
|
15
|
+
headers,
|
|
16
|
+
body: JSON.stringify(body)
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const result = await res.json().catch(() => ({}));
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
const message =
|
|
22
|
+
result?.error?.message || result?.message || `HTTP ${res.status}`;
|
|
23
|
+
throw new Error(message);
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function resolveDid(tokenId, { baseUrl = DEFAULT_BASE } = {}) {
|
|
29
|
+
const normalized = String(tokenId || "").trim().toLowerCase();
|
|
30
|
+
if (!/^[a-z]{12}$/.test(normalized)) {
|
|
31
|
+
throw new Error("tokenId must be 12 lowercase letters");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const res = await fetch(
|
|
35
|
+
`${baseUrl.replace(/\/$/, "")}/api/mcp/resource/did:resolve:${normalized}`,
|
|
36
|
+
{ headers: { Accept: "application/json" } }
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const payload = await res.json().catch(() => ({}));
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
throw new Error(payload?.error?.message || `DID resolve HTTP ${res.status}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const content = payload?.content ?? payload;
|
|
45
|
+
return typeof content === "string" ? JSON.parse(content) : content;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function extractProfileFromDid(didDocument) {
|
|
49
|
+
const metadataService = (didDocument?.service || []).find(
|
|
50
|
+
(s) => s.type === "RoditTokenMetadata"
|
|
51
|
+
);
|
|
52
|
+
const metadata = metadataService?.serviceEndpoint?.metadata || {};
|
|
53
|
+
const rawDn = metadata.userselected_dn || null;
|
|
54
|
+
const parsed = parseUserSelectedDn(rawDn);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
tokenId: metadataService?.serviceEndpoint?.tokenId || didDocument?.id?.split(":").pop() || null,
|
|
58
|
+
ownerAccountId: metadataService?.serviceEndpoint?.ownerAccountId || didDocument?.controller || null,
|
|
59
|
+
did: didDocument?.id || null,
|
|
60
|
+
displayName: parsed.displayName,
|
|
61
|
+
creature: parsed.creature,
|
|
62
|
+
contactUris: parsed.contactUris,
|
|
63
|
+
webhookUrl: metadata.webhook_url || parsed.webhookUrl || null,
|
|
64
|
+
dn: parsed
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function buildHolaReport(hola, options = {}) {
|
|
69
|
+
const {
|
|
70
|
+
baseUrl = DEFAULT_BASE,
|
|
71
|
+
jwt,
|
|
72
|
+
expectedRecipient,
|
|
73
|
+
canonicalPeerId,
|
|
74
|
+
includeProfile = true
|
|
75
|
+
} = options;
|
|
76
|
+
|
|
77
|
+
const verifyResult = await verifyHola(hola, {
|
|
78
|
+
baseUrl,
|
|
79
|
+
jwt,
|
|
80
|
+
expectedRecipient,
|
|
81
|
+
includeProfile
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const report = {
|
|
85
|
+
verified: Boolean(verifyResult.verified),
|
|
86
|
+
peerTokenId: verifyResult.peerTokenId || null,
|
|
87
|
+
destinatary: verifyResult.destinatary || null,
|
|
88
|
+
checks: verifyResult.checks || {},
|
|
89
|
+
failureReasons: verifyResult.failureReasons || [],
|
|
90
|
+
failureDetails: verifyResult.failureDetails || [],
|
|
91
|
+
warnings: verifyResult.warnings || [],
|
|
92
|
+
requestId: verifyResult.requestId || null,
|
|
93
|
+
impersonationGuard: null,
|
|
94
|
+
profile: null
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (canonicalPeerId && report.verified) {
|
|
98
|
+
const expected = String(canonicalPeerId).trim().toLowerCase();
|
|
99
|
+
const matched = report.peerTokenId === expected;
|
|
100
|
+
report.impersonationGuard = {
|
|
101
|
+
expectedTokenId: expected,
|
|
102
|
+
matched,
|
|
103
|
+
passed: matched
|
|
104
|
+
};
|
|
105
|
+
if (!matched) {
|
|
106
|
+
report.verified = false;
|
|
107
|
+
report.failureReasons = [...(report.failureReasons || []), "impersonation_guard_failed"];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (includeProfile && report.peerTokenId && !verifyResult.profile) {
|
|
112
|
+
try {
|
|
113
|
+
const didDocument = await resolveDid(report.peerTokenId, { baseUrl });
|
|
114
|
+
report.profile = extractProfileFromDid(didDocument);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
report.profile = { error: error.message };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (verifyResult.profile) {
|
|
121
|
+
report.profile = { ...report.profile, ...verifyResult.profile };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return report;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
DEFAULT_BASE,
|
|
129
|
+
verifyHola,
|
|
130
|
+
resolveDid,
|
|
131
|
+
extractProfileFromDid,
|
|
132
|
+
buildHolaReport
|
|
133
|
+
};
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
const base32 = require("hi-base32");
|
|
2
|
+
const nacl = require("tweetnacl");
|
|
3
|
+
const { validateHolaMessage } = require("./hola-validate");
|
|
4
|
+
const { parseUserSelectedDn } = require("./parse-dn");
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MAX_HOLA_AGE_MS = 5 * 60 * 1000;
|
|
7
|
+
const nonceReplayCache = new Map();
|
|
8
|
+
|
|
9
|
+
function decodeBase32ToBytes(value) {
|
|
10
|
+
const raw = String(value).toUpperCase().replace(/=+$/g, "");
|
|
11
|
+
if (!/^[A-Z2-7]+$/.test(raw)) {
|
|
12
|
+
throw new Error("Invalid base32 input");
|
|
13
|
+
}
|
|
14
|
+
const padded = raw.padEnd(Math.ceil(raw.length / 8) * 8, "=");
|
|
15
|
+
return new Uint8Array(base32.decode.asBytes(padded));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isRoditTokenExpired(token) {
|
|
19
|
+
const notAfter = token?.metadata?.not_after;
|
|
20
|
+
if (!notAfter || String(notAfter).startsWith("1970-01-01")) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const notAfterMs = Date.parse(String(notAfter));
|
|
24
|
+
if (Number.isNaN(notAfterMs)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return Date.now() > notAfterMs;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function cleanupExpiredVerifyNonceEntries() {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
for (const [key, entry] of nonceReplayCache.entries()) {
|
|
33
|
+
if (!entry || entry.expiresAtMs <= now) {
|
|
34
|
+
nonceReplayCache.delete(key);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildProfileFromToken(token, tokenId) {
|
|
40
|
+
const metadata = token?.metadata || {};
|
|
41
|
+
const parsed = parseUserSelectedDn(metadata.userselected_dn || "");
|
|
42
|
+
return {
|
|
43
|
+
tokenId: String(tokenId).toLowerCase(),
|
|
44
|
+
did: `did:rodit:${String(tokenId).toLowerCase()}`,
|
|
45
|
+
ownerAccountId: token?.owner_id || null,
|
|
46
|
+
displayName: parsed.displayName,
|
|
47
|
+
creature: parsed.creature,
|
|
48
|
+
contactUris: parsed.contactUris,
|
|
49
|
+
webhookUrl: metadata.webhook_url || parsed.webhookUrl || null
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ensureNearEnv({ nearRpcUrl, nearContractId } = {}) {
|
|
54
|
+
if (nearRpcUrl) process.env.NEAR_RPC_URL = nearRpcUrl;
|
|
55
|
+
if (nearContractId) process.env.NEAR_CONTRACT_ID = nearContractId;
|
|
56
|
+
|
|
57
|
+
const rpc = process.env.NEAR_RPC_URL;
|
|
58
|
+
const contract = process.env.NEAR_CONTRACT_ID;
|
|
59
|
+
if (!rpc) {
|
|
60
|
+
throw new Error("NEAR_RPC_URL is required for --rpc verification (e.g. https://rpc.mainnet.fastnear.com)");
|
|
61
|
+
}
|
|
62
|
+
if (!contract) {
|
|
63
|
+
throw new Error("NEAR_CONTRACT_ID is required for --rpc verification (e.g. genaaaa-identyclaw-com.near)");
|
|
64
|
+
}
|
|
65
|
+
return { rpc, contract };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function verifyHolaViaRpc(hola, options = {}) {
|
|
69
|
+
const {
|
|
70
|
+
maxAgeMs = DEFAULT_MAX_HOLA_AGE_MS,
|
|
71
|
+
nearRpcUrl,
|
|
72
|
+
nearContractId
|
|
73
|
+
} = options;
|
|
74
|
+
|
|
75
|
+
ensureNearEnv({ nearRpcUrl, nearContractId });
|
|
76
|
+
|
|
77
|
+
const validation = validateHolaMessage(hola);
|
|
78
|
+
if (!validation.valid) {
|
|
79
|
+
return {
|
|
80
|
+
verified: false,
|
|
81
|
+
verifyPath: "near-rpc",
|
|
82
|
+
peerTokenId: validation.tokenId ? String(validation.tokenId).toLowerCase() : null,
|
|
83
|
+
destinatary: validation.recipient,
|
|
84
|
+
checks: {
|
|
85
|
+
tokenExists: false,
|
|
86
|
+
tokenActive: false,
|
|
87
|
+
signatureValid: false,
|
|
88
|
+
timestampFresh: false,
|
|
89
|
+
checksumValid: Boolean(validation.checks?.checksumValid),
|
|
90
|
+
nonceReplaySafe: true
|
|
91
|
+
},
|
|
92
|
+
failureReasons: ["hola_format_invalid"],
|
|
93
|
+
failureDetails: [{ reasonCode: "hola_format_invalid", message: validation.reason }],
|
|
94
|
+
profile: null
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const lookupTokenId = String(validation.tokenId).toLowerCase();
|
|
99
|
+
const checks = {
|
|
100
|
+
tokenExists: false,
|
|
101
|
+
tokenActive: false,
|
|
102
|
+
signatureValid: false,
|
|
103
|
+
timestampFresh: false,
|
|
104
|
+
checksumValid: true,
|
|
105
|
+
nonceReplaySafe: true
|
|
106
|
+
};
|
|
107
|
+
const failureReasons = [];
|
|
108
|
+
|
|
109
|
+
const tsMs = Date.parse(validation.isoTimestamp);
|
|
110
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
111
|
+
const tsSeconds = Math.floor(tsMs / 1000);
|
|
112
|
+
const ageSeconds = nowSeconds - tsSeconds;
|
|
113
|
+
const maxAgeSeconds = Math.floor(maxAgeMs / 1000);
|
|
114
|
+
if (ageSeconds >= 0 && ageSeconds <= maxAgeSeconds) {
|
|
115
|
+
checks.timestampFresh = true;
|
|
116
|
+
} else {
|
|
117
|
+
failureReasons.push("timestamp_stale_or_future");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
cleanupExpiredVerifyNonceEntries();
|
|
121
|
+
const nonceReplayKey = `${lookupTokenId}|${String(validation.noncetsHex).toUpperCase()}`;
|
|
122
|
+
if (nonceReplayCache.has(nonceReplayKey)) {
|
|
123
|
+
checks.nonceReplaySafe = false;
|
|
124
|
+
failureReasons.push("nonce_replay");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const { blockchainService } = require("@rodit/rodit-auth-be");
|
|
128
|
+
let token = null;
|
|
129
|
+
let profile = null;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
token = await blockchainService.nearorg_rpc_tokenfromroditid(lookupTokenId);
|
|
133
|
+
if (token && token.token_id) {
|
|
134
|
+
checks.tokenExists = true;
|
|
135
|
+
checks.tokenActive = !isRoditTokenExpired(token);
|
|
136
|
+
profile = buildProfileFromToken(token, lookupTokenId);
|
|
137
|
+
if (!checks.tokenActive) {
|
|
138
|
+
failureReasons.push("token_expired");
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
failureReasons.push("token_missing");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (token && token.owner_id) {
|
|
145
|
+
let publicKeyBytes;
|
|
146
|
+
if (validation.isSubagentFormat) {
|
|
147
|
+
try {
|
|
148
|
+
publicKeyBytes = decodeBase32ToBytes(validation.subagentPublicKey);
|
|
149
|
+
if (publicKeyBytes.length !== 32) {
|
|
150
|
+
failureReasons.push("subagent_public_key_invalid_length");
|
|
151
|
+
publicKeyBytes = null;
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
failureReasons.push("subagent_public_key_decode_failed");
|
|
155
|
+
publicKeyBytes = null;
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
publicKeyBytes = await blockchainService.nearorg_rpc_fetchpublickeybytes(token.owner_id);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (publicKeyBytes && publicKeyBytes.length === 32) {
|
|
162
|
+
const message = validation.isSubagentFormat
|
|
163
|
+
? `HOLA/${validation.recipient}/${validation.delegateId}/${validation.issuerTokenId}/${validation.subagentPublicKey}/${validation.isoTimestamp}/${validation.noncetsHex}/API.IDENTYCLAW.COM/`
|
|
164
|
+
: `HOLA/${validation.recipient}/${validation.tokenId}/${validation.isoTimestamp}/${validation.noncetsHex}/API.IDENTYCLAW.COM/`;
|
|
165
|
+
const canonicalMessage = message.toUpperCase();
|
|
166
|
+
const messageBytes = new TextEncoder().encode(canonicalMessage);
|
|
167
|
+
const signatureBytes = decodeBase32ToBytes(validation.signatureB32);
|
|
168
|
+
if (nacl.sign.detached.verify(messageBytes, signatureBytes, publicKeyBytes)) {
|
|
169
|
+
checks.signatureValid = true;
|
|
170
|
+
} else {
|
|
171
|
+
failureReasons.push("signature_invalid");
|
|
172
|
+
}
|
|
173
|
+
} else if (!validation.isSubagentFormat) {
|
|
174
|
+
failureReasons.push("public_key_unavailable");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
failureReasons.push("public_key_error");
|
|
179
|
+
return {
|
|
180
|
+
verified: false,
|
|
181
|
+
verifyPath: "near-rpc",
|
|
182
|
+
peerTokenId: lookupTokenId,
|
|
183
|
+
destinatary: validation.recipient,
|
|
184
|
+
checks,
|
|
185
|
+
failureReasons,
|
|
186
|
+
failureDetails: [{ reasonCode: "public_key_error", message: error.message }],
|
|
187
|
+
profile
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const verified =
|
|
192
|
+
checks.tokenExists &&
|
|
193
|
+
checks.tokenActive &&
|
|
194
|
+
checks.timestampFresh &&
|
|
195
|
+
checks.nonceReplaySafe &&
|
|
196
|
+
checks.signatureValid &&
|
|
197
|
+
checks.checksumValid;
|
|
198
|
+
|
|
199
|
+
if (verified) {
|
|
200
|
+
nonceReplayCache.set(nonceReplayKey, {
|
|
201
|
+
expiresAtMs: tsMs + maxAgeMs,
|
|
202
|
+
firstSeenAtMs: Date.now()
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const response = {
|
|
207
|
+
verified,
|
|
208
|
+
verifyPath: "near-rpc",
|
|
209
|
+
nearRpcUrl: process.env.NEAR_RPC_URL,
|
|
210
|
+
nearContractId: process.env.NEAR_CONTRACT_ID,
|
|
211
|
+
peerTokenId: lookupTokenId,
|
|
212
|
+
destinatary: validation.recipient,
|
|
213
|
+
checks,
|
|
214
|
+
failureReasons,
|
|
215
|
+
profile
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
if (validation.isSubagentFormat) {
|
|
219
|
+
response.isSubagentFormat = true;
|
|
220
|
+
response.delegateId = validation.delegateId;
|
|
221
|
+
response.issuerTokenId = validation.issuerTokenId;
|
|
222
|
+
response.subagentNote =
|
|
223
|
+
"Subagent delegation requires POST /api/isauthorizedsigner — not checked on --rpc path";
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return response;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function buildHolaReportViaRpc(hola, options = {}) {
|
|
230
|
+
const { canonicalPeerId, ...rpcOptions } = options;
|
|
231
|
+
const report = await verifyHolaViaRpc(hola, rpcOptions);
|
|
232
|
+
|
|
233
|
+
if (canonicalPeerId && report.verified) {
|
|
234
|
+
const expected = String(canonicalPeerId).trim().toLowerCase();
|
|
235
|
+
const matched = report.peerTokenId === expected;
|
|
236
|
+
report.impersonationGuard = { expectedTokenId: expected, matched, passed: matched };
|
|
237
|
+
if (!matched) {
|
|
238
|
+
report.verified = false;
|
|
239
|
+
report.failureReasons = [...(report.failureReasons || []), "impersonation_guard_failed"];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return report;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
module.exports = {
|
|
247
|
+
verifyHolaViaRpc,
|
|
248
|
+
buildHolaReportViaRpc,
|
|
249
|
+
ensureNearEnv
|
|
250
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rodit/verify-hola",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Verify HOLA lines and build passport reports via api.identyclaw.com (no JWT required for verify)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"main": "lib/report.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"verify-hola": "bin/verify-hola.js"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@rodit/rodit-auth-be": "9.17.4",
|
|
13
|
+
"hi-base32": "^0.5.1",
|
|
14
|
+
"tweetnacl": "^1.0.3"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"bin",
|
|
21
|
+
"lib",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"keywords": [
|
|
25
|
+
"identyclaw",
|
|
26
|
+
"hola",
|
|
27
|
+
"agent-identity",
|
|
28
|
+
"verification",
|
|
29
|
+
"near",
|
|
30
|
+
"rodit"
|
|
31
|
+
],
|
|
32
|
+
"author": "Discernible IO",
|
|
33
|
+
"private": false,
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/discernible-io/rodit-sdk.git",
|
|
37
|
+
"directory": "verify-hola"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/discernible-io/rodit-sdk/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/discernible-io/rodit-sdk/tree/main/verify-hola#readme",
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"test": "node --test test/report.test.js"
|
|
48
|
+
}
|
|
49
|
+
}
|