@rare-id/platform-kit-core 0.1.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/dist/index.d.ts +73 -0
- package/dist/index.js +252 -0
- package/package.json +37 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
type IdentityLevel = "L0" | "L1" | "L2";
|
|
2
|
+
interface VerifiedTokenResult {
|
|
3
|
+
header: Record<string, unknown>;
|
|
4
|
+
payload: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
interface RareJwks {
|
|
7
|
+
issuer?: string;
|
|
8
|
+
keys?: Array<Record<string, unknown>>;
|
|
9
|
+
}
|
|
10
|
+
interface RareJwk {
|
|
11
|
+
kid: string;
|
|
12
|
+
kty: "OKP";
|
|
13
|
+
crv: "Ed25519";
|
|
14
|
+
x: string;
|
|
15
|
+
}
|
|
16
|
+
type KeyResolver = (kid: string) => Promise<RareJwk | null> | RareJwk | null;
|
|
17
|
+
declare function parseRareJwks(jwks: RareJwks): Record<string, RareJwk>;
|
|
18
|
+
interface VerifyIdentityOptions {
|
|
19
|
+
keyResolver: KeyResolver;
|
|
20
|
+
expectedAud?: string;
|
|
21
|
+
currentTs?: number;
|
|
22
|
+
clockSkewSeconds?: number;
|
|
23
|
+
}
|
|
24
|
+
declare function verifyIdentityAttestation(token: string, options: VerifyIdentityOptions): Promise<VerifiedTokenResult>;
|
|
25
|
+
interface VerifyDelegationOptions {
|
|
26
|
+
expectedAud: string;
|
|
27
|
+
requiredScope: string;
|
|
28
|
+
rareSignerPublicKeyB64?: string;
|
|
29
|
+
currentTs?: number;
|
|
30
|
+
clockSkewSeconds?: number;
|
|
31
|
+
}
|
|
32
|
+
declare function verifyDelegationToken(token: string, options: VerifyDelegationOptions): Promise<VerifiedTokenResult>;
|
|
33
|
+
declare function buildAuthChallengePayload(input: {
|
|
34
|
+
aud: string;
|
|
35
|
+
nonce: string;
|
|
36
|
+
issuedAt: number;
|
|
37
|
+
expiresAt: number;
|
|
38
|
+
}): string;
|
|
39
|
+
declare function buildActionPayload(input: {
|
|
40
|
+
aud: string;
|
|
41
|
+
sessionToken: string;
|
|
42
|
+
action: string;
|
|
43
|
+
actionPayload: Record<string, unknown>;
|
|
44
|
+
nonce: string;
|
|
45
|
+
issuedAt: number;
|
|
46
|
+
expiresAt: number;
|
|
47
|
+
}): string;
|
|
48
|
+
declare function stableJson(value: unknown): string;
|
|
49
|
+
declare function verifyDetached(input: string, signatureB64Url: string, publicKeyB64Url: string): boolean;
|
|
50
|
+
interface RarePlatformEventItem {
|
|
51
|
+
event_id: string;
|
|
52
|
+
agent_id: string;
|
|
53
|
+
category: "spam" | "fraud" | "abuse" | "policy_violation";
|
|
54
|
+
severity: number;
|
|
55
|
+
outcome: string;
|
|
56
|
+
occurred_at: number;
|
|
57
|
+
evidence_hash?: string;
|
|
58
|
+
}
|
|
59
|
+
interface SignPlatformEventTokenInput {
|
|
60
|
+
platformId: string;
|
|
61
|
+
kid: string;
|
|
62
|
+
privateKeyPem: string;
|
|
63
|
+
jti: string;
|
|
64
|
+
events: RarePlatformEventItem[];
|
|
65
|
+
issuedAt?: number;
|
|
66
|
+
expiresAt?: number;
|
|
67
|
+
}
|
|
68
|
+
declare function signPlatformEventToken(input: SignPlatformEventTokenInput): Promise<string>;
|
|
69
|
+
declare function nowTs(): number;
|
|
70
|
+
declare function generateNonce(size?: number): string;
|
|
71
|
+
declare function decodeBase64Url(value: string): Uint8Array;
|
|
72
|
+
|
|
73
|
+
export { type IdentityLevel, type KeyResolver, type RareJwk, type RareJwks, type RarePlatformEventItem, type SignPlatformEventTokenInput, type VerifiedTokenResult, type VerifyDelegationOptions, type VerifyIdentityOptions, buildActionPayload, buildAuthChallengePayload, decodeBase64Url, generateNonce, nowTs, parseRareJwks, signPlatformEventToken, stableJson, verifyDelegationToken, verifyDetached, verifyIdentityAttestation };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { createHash, randomBytes } from "crypto";
|
|
3
|
+
import {
|
|
4
|
+
SignJWT,
|
|
5
|
+
decodeJwt,
|
|
6
|
+
decodeProtectedHeader,
|
|
7
|
+
importJWK,
|
|
8
|
+
importPKCS8,
|
|
9
|
+
jwtVerify
|
|
10
|
+
} from "jose";
|
|
11
|
+
import nacl from "tweetnacl";
|
|
12
|
+
function parseRareJwks(jwks) {
|
|
13
|
+
if (!Array.isArray(jwks.keys)) {
|
|
14
|
+
throw new Error("invalid JWKS payload");
|
|
15
|
+
}
|
|
16
|
+
const resolved = {};
|
|
17
|
+
for (const item of jwks.keys) {
|
|
18
|
+
if (!item || typeof item !== "object") {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const kid = item.kid;
|
|
22
|
+
const kty = item.kty;
|
|
23
|
+
const crv = item.crv;
|
|
24
|
+
const x = item.x;
|
|
25
|
+
if (typeof kid !== "string" || typeof x !== "string") {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (kty !== "OKP" || crv !== "Ed25519") {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
resolved[kid] = { kid, kty, crv, x };
|
|
32
|
+
}
|
|
33
|
+
return resolved;
|
|
34
|
+
}
|
|
35
|
+
async function verifyIdentityAttestation(token, options) {
|
|
36
|
+
const header = decodeProtectedHeader(token);
|
|
37
|
+
const typ = header.typ;
|
|
38
|
+
if (typ !== "rare.identity.public+jws" && typ !== "rare.identity.full+jws") {
|
|
39
|
+
throw new Error("invalid identity token typ");
|
|
40
|
+
}
|
|
41
|
+
const kid = header.kid;
|
|
42
|
+
if (typeof kid !== "string") {
|
|
43
|
+
throw new Error("missing key id");
|
|
44
|
+
}
|
|
45
|
+
const resolved = await options.keyResolver(kid);
|
|
46
|
+
if (!resolved) {
|
|
47
|
+
throw new Error("unknown identity key id");
|
|
48
|
+
}
|
|
49
|
+
const key = await importJWK(
|
|
50
|
+
{
|
|
51
|
+
kty: "OKP",
|
|
52
|
+
crv: "Ed25519",
|
|
53
|
+
x: resolved.x
|
|
54
|
+
},
|
|
55
|
+
"EdDSA"
|
|
56
|
+
);
|
|
57
|
+
const verified = await jwtVerify(token, key, {
|
|
58
|
+
algorithms: ["EdDSA"],
|
|
59
|
+
clockTolerance: options.clockSkewSeconds ?? 30
|
|
60
|
+
});
|
|
61
|
+
const payload = verified.payload;
|
|
62
|
+
if (payload.typ !== "rare.identity") {
|
|
63
|
+
throw new Error("invalid identity payload typ");
|
|
64
|
+
}
|
|
65
|
+
if (payload.ver !== 1) {
|
|
66
|
+
throw new Error("unsupported identity payload version");
|
|
67
|
+
}
|
|
68
|
+
if (payload.iss !== "rare") {
|
|
69
|
+
throw new Error("invalid identity issuer");
|
|
70
|
+
}
|
|
71
|
+
if (typ === "rare.identity.full+jws") {
|
|
72
|
+
if (!options.expectedAud) {
|
|
73
|
+
throw new Error("expected_aud required for full identity token");
|
|
74
|
+
}
|
|
75
|
+
if (payload.aud !== options.expectedAud) {
|
|
76
|
+
throw new Error("identity full token aud mismatch");
|
|
77
|
+
}
|
|
78
|
+
} else if (Object.prototype.hasOwnProperty.call(payload, "aud")) {
|
|
79
|
+
throw new Error("public identity token must not contain aud");
|
|
80
|
+
}
|
|
81
|
+
const level = payload.lvl;
|
|
82
|
+
if (level !== "L0" && level !== "L1" && level !== "L2") {
|
|
83
|
+
throw new Error("invalid identity level");
|
|
84
|
+
}
|
|
85
|
+
const now = options.currentTs ?? nowTs();
|
|
86
|
+
const iat = payload.iat;
|
|
87
|
+
const exp = payload.exp;
|
|
88
|
+
if (typeof iat !== "number" || typeof exp !== "number") {
|
|
89
|
+
throw new Error("identity timestamps must be integers");
|
|
90
|
+
}
|
|
91
|
+
const skew = options.clockSkewSeconds ?? 30;
|
|
92
|
+
if (iat - skew > now) {
|
|
93
|
+
throw new Error("identity token not yet valid");
|
|
94
|
+
}
|
|
95
|
+
if (exp + skew < now) {
|
|
96
|
+
throw new Error("identity token expired");
|
|
97
|
+
}
|
|
98
|
+
return { header, payload };
|
|
99
|
+
}
|
|
100
|
+
async function verifyDelegationToken(token, options) {
|
|
101
|
+
const header = decodeProtectedHeader(token);
|
|
102
|
+
if (header.typ !== "rare.delegation+jws") {
|
|
103
|
+
throw new Error("invalid delegation token typ");
|
|
104
|
+
}
|
|
105
|
+
const payload = decodeJwt(token);
|
|
106
|
+
if (payload.typ !== "rare.delegation") {
|
|
107
|
+
throw new Error("invalid delegation payload typ");
|
|
108
|
+
}
|
|
109
|
+
if (payload.ver !== 1) {
|
|
110
|
+
throw new Error("unsupported delegation payload version");
|
|
111
|
+
}
|
|
112
|
+
const agentId = payload.agent_id;
|
|
113
|
+
if (typeof agentId !== "string") {
|
|
114
|
+
throw new Error("delegation agent_id missing");
|
|
115
|
+
}
|
|
116
|
+
let key;
|
|
117
|
+
if (payload.iss === "rare-signer") {
|
|
118
|
+
if (payload.act !== "delegated_by_rare") {
|
|
119
|
+
throw new Error("rare signer delegation missing act");
|
|
120
|
+
}
|
|
121
|
+
if (!options.rareSignerPublicKeyB64) {
|
|
122
|
+
throw new Error("rare signer key unavailable");
|
|
123
|
+
}
|
|
124
|
+
key = await importJWK(
|
|
125
|
+
{ kty: "OKP", crv: "Ed25519", x: options.rareSignerPublicKeyB64 },
|
|
126
|
+
"EdDSA"
|
|
127
|
+
);
|
|
128
|
+
} else if (payload.iss === "agent") {
|
|
129
|
+
if (payload.act !== "delegated_by_agent") {
|
|
130
|
+
throw new Error("agent delegation missing act");
|
|
131
|
+
}
|
|
132
|
+
key = await importJWK({ kty: "OKP", crv: "Ed25519", x: agentId }, "EdDSA");
|
|
133
|
+
} else {
|
|
134
|
+
throw new Error("unsupported delegation issuer");
|
|
135
|
+
}
|
|
136
|
+
const verified = await jwtVerify(token, key, {
|
|
137
|
+
algorithms: ["EdDSA"],
|
|
138
|
+
clockTolerance: options.clockSkewSeconds ?? 30
|
|
139
|
+
});
|
|
140
|
+
const verifiedPayload = verified.payload;
|
|
141
|
+
if (verifiedPayload.aud !== options.expectedAud) {
|
|
142
|
+
throw new Error("delegation aud mismatch");
|
|
143
|
+
}
|
|
144
|
+
const scope = verifiedPayload.scope;
|
|
145
|
+
if (!Array.isArray(scope) || !scope.includes(options.requiredScope)) {
|
|
146
|
+
throw new Error("delegation scope missing required action");
|
|
147
|
+
}
|
|
148
|
+
if (typeof verifiedPayload.session_pubkey !== "string") {
|
|
149
|
+
throw new Error("delegation missing session_pubkey");
|
|
150
|
+
}
|
|
151
|
+
const now = options.currentTs ?? nowTs();
|
|
152
|
+
const iat = verifiedPayload.iat;
|
|
153
|
+
const exp = verifiedPayload.exp;
|
|
154
|
+
const jti = verifiedPayload.jti;
|
|
155
|
+
if (typeof iat !== "number" || typeof exp !== "number") {
|
|
156
|
+
throw new Error("delegation timestamps must be integers");
|
|
157
|
+
}
|
|
158
|
+
if (typeof jti !== "string" || jti.trim().length === 0) {
|
|
159
|
+
throw new Error("delegation jti missing");
|
|
160
|
+
}
|
|
161
|
+
const skew = options.clockSkewSeconds ?? 30;
|
|
162
|
+
if (iat - skew > now) {
|
|
163
|
+
throw new Error("delegation token not yet valid");
|
|
164
|
+
}
|
|
165
|
+
if (exp + skew < now) {
|
|
166
|
+
throw new Error("delegation token expired");
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
header,
|
|
170
|
+
payload: verifiedPayload
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function buildAuthChallengePayload(input) {
|
|
174
|
+
return `rare-auth-v1:${input.aud}:${input.nonce}:${input.issuedAt}:${input.expiresAt}`;
|
|
175
|
+
}
|
|
176
|
+
function buildActionPayload(input) {
|
|
177
|
+
const bodyHash = sha256Hex(stableJson(input.actionPayload));
|
|
178
|
+
return `rare-act-v1:${input.aud}:${input.sessionToken}:${input.action}:${bodyHash}:${input.nonce}:${input.issuedAt}:${input.expiresAt}`;
|
|
179
|
+
}
|
|
180
|
+
function stableJson(value) {
|
|
181
|
+
return JSON.stringify(sortJson(value));
|
|
182
|
+
}
|
|
183
|
+
function sortJson(value) {
|
|
184
|
+
if (Array.isArray(value)) {
|
|
185
|
+
return value.map((item) => sortJson(item));
|
|
186
|
+
}
|
|
187
|
+
if (value && typeof value === "object") {
|
|
188
|
+
const entries = Object.entries(value).sort(
|
|
189
|
+
([a], [b]) => a < b ? -1 : a > b ? 1 : 0
|
|
190
|
+
);
|
|
191
|
+
const output = {};
|
|
192
|
+
for (const [k, v] of entries) {
|
|
193
|
+
output[k] = sortJson(v);
|
|
194
|
+
}
|
|
195
|
+
return output;
|
|
196
|
+
}
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
function sha256Hex(input) {
|
|
200
|
+
const bytes = new TextEncoder().encode(input);
|
|
201
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
202
|
+
}
|
|
203
|
+
function verifyDetached(input, signatureB64Url, publicKeyB64Url) {
|
|
204
|
+
const message = new TextEncoder().encode(input);
|
|
205
|
+
const signature = decodeBase64Url(signatureB64Url);
|
|
206
|
+
const publicKey = decodeBase64Url(publicKeyB64Url);
|
|
207
|
+
return nacl.sign.detached.verify(message, signature, publicKey);
|
|
208
|
+
}
|
|
209
|
+
async function signPlatformEventToken(input) {
|
|
210
|
+
const issuedAt = input.issuedAt ?? nowTs();
|
|
211
|
+
const expiresAt = input.expiresAt ?? issuedAt + 300;
|
|
212
|
+
const key = await importPKCS8(input.privateKeyPem, "EdDSA");
|
|
213
|
+
const payload = {
|
|
214
|
+
typ: "rare.platform-event",
|
|
215
|
+
ver: 1,
|
|
216
|
+
iss: input.platformId,
|
|
217
|
+
aud: "rare.identity-library",
|
|
218
|
+
iat: issuedAt,
|
|
219
|
+
exp: expiresAt,
|
|
220
|
+
jti: input.jti,
|
|
221
|
+
events: input.events
|
|
222
|
+
};
|
|
223
|
+
return new SignJWT(payload).setProtectedHeader({
|
|
224
|
+
alg: "EdDSA",
|
|
225
|
+
typ: "rare.platform-event+jws",
|
|
226
|
+
kid: input.kid
|
|
227
|
+
}).sign(key);
|
|
228
|
+
}
|
|
229
|
+
function nowTs() {
|
|
230
|
+
return Math.floor(Date.now() / 1e3);
|
|
231
|
+
}
|
|
232
|
+
function generateNonce(size = 18) {
|
|
233
|
+
return randomBytes(size).toString("base64url");
|
|
234
|
+
}
|
|
235
|
+
function decodeBase64Url(value) {
|
|
236
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
237
|
+
const pad = normalized.length % 4 === 0 ? "" : "=".repeat(4 - normalized.length % 4);
|
|
238
|
+
return new Uint8Array(Buffer.from(normalized + pad, "base64"));
|
|
239
|
+
}
|
|
240
|
+
export {
|
|
241
|
+
buildActionPayload,
|
|
242
|
+
buildAuthChallengePayload,
|
|
243
|
+
decodeBase64Url,
|
|
244
|
+
generateNonce,
|
|
245
|
+
nowTs,
|
|
246
|
+
parseRareJwks,
|
|
247
|
+
signPlatformEventToken,
|
|
248
|
+
stableJson,
|
|
249
|
+
verifyDelegationToken,
|
|
250
|
+
verifyDetached,
|
|
251
|
+
verifyIdentityAttestation
|
|
252
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rare-id/platform-kit-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/0xsidfan/Rare.git",
|
|
21
|
+
"directory": "rare-platform-kit-ts/packages/platform-kit-core"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public",
|
|
25
|
+
"provenance": false
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"jose": "^6.0.8",
|
|
29
|
+
"tweetnacl": "^1.0.3"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"lint": "biome check src test",
|
|
35
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
36
|
+
}
|
|
37
|
+
}
|