@rare-id/platform-kit-web 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.
@@ -0,0 +1,123 @@
1
+ import { RareApiClient } from '@rare-id/platform-kit-client';
2
+ import { IdentityLevel, RarePlatformEventItem, KeyResolver } from '@rare-id/platform-kit-core';
3
+
4
+ type IdentityMode = "public" | "full";
5
+ type EffectiveLevel = "L0" | "L1" | "L2";
6
+ interface AuthChallenge {
7
+ nonce: string;
8
+ aud: string;
9
+ issuedAt: number;
10
+ expiresAt: number;
11
+ }
12
+ interface PlatformSession {
13
+ sessionToken: string;
14
+ agentId: string;
15
+ sessionPubkey: string;
16
+ identityMode: IdentityMode;
17
+ rawLevel: IdentityLevel;
18
+ effectiveLevel: EffectiveLevel;
19
+ displayName: string;
20
+ aud: string;
21
+ createdAt: number;
22
+ expiresAt: number;
23
+ }
24
+ interface AuthCompleteInput {
25
+ nonce: string;
26
+ agentId: string;
27
+ sessionPubkey: string;
28
+ delegationToken: string;
29
+ signatureBySession: string;
30
+ publicIdentityAttestation?: string;
31
+ fullIdentityAttestation?: string;
32
+ }
33
+ interface AuthCompleteResult {
34
+ session_token: string;
35
+ agent_id: string;
36
+ level: EffectiveLevel;
37
+ raw_level: IdentityLevel;
38
+ identity_mode: IdentityMode;
39
+ display_name: string;
40
+ session_pubkey: string;
41
+ }
42
+ interface VerifyActionInput {
43
+ sessionToken: string;
44
+ action: string;
45
+ actionPayload: Record<string, unknown>;
46
+ nonce: string;
47
+ issuedAt: number;
48
+ expiresAt: number;
49
+ signatureBySession: string;
50
+ }
51
+ interface VerifiedActionContext {
52
+ session: PlatformSession;
53
+ action: string;
54
+ actionPayload: Record<string, unknown>;
55
+ }
56
+ interface IngestEventsInput {
57
+ eventToken?: string;
58
+ platformId?: string;
59
+ kid?: string;
60
+ privateKeyPem?: string;
61
+ jti?: string;
62
+ events?: RarePlatformEventItem[];
63
+ issuedAt?: number;
64
+ expiresAt?: number;
65
+ }
66
+ interface IngestEventsResult {
67
+ eventToken: string;
68
+ response: Record<string, unknown>;
69
+ }
70
+ interface ChallengeStore {
71
+ set(challenge: AuthChallenge): Promise<void>;
72
+ consume(nonce: string): Promise<AuthChallenge | null>;
73
+ }
74
+ interface ReplayStore {
75
+ has(key: string): Promise<boolean>;
76
+ put(key: string, expiresAt: number): Promise<void>;
77
+ }
78
+ interface SessionStore {
79
+ save(session: PlatformSession): Promise<void>;
80
+ get(sessionToken: string): Promise<PlatformSession | null>;
81
+ }
82
+ interface RarePlatformKit {
83
+ issueChallenge(aud?: string): Promise<AuthChallenge>;
84
+ completeAuth(input: AuthCompleteInput): Promise<AuthCompleteResult>;
85
+ verifyAction(input: VerifyActionInput): Promise<VerifiedActionContext>;
86
+ ingestNegativeEvents(input: IngestEventsInput): Promise<IngestEventsResult>;
87
+ }
88
+ interface RarePlatformKitConfig {
89
+ aud: string;
90
+ challengeStore: ChallengeStore;
91
+ replayStore: ReplayStore;
92
+ sessionStore: SessionStore;
93
+ rareApiClient?: RareApiClient;
94
+ keyResolver?: KeyResolver;
95
+ initialJwks?: {
96
+ issuer?: string;
97
+ keys?: Array<Record<string, unknown>>;
98
+ };
99
+ rareSignerPublicKeyB64?: string;
100
+ challengeTtlSeconds?: number;
101
+ sessionTtlSeconds?: number;
102
+ maxSignedTtlSeconds?: number;
103
+ clockSkewSeconds?: number;
104
+ }
105
+ declare function createRarePlatformKit(config: RarePlatformKitConfig): RarePlatformKit;
106
+ declare class InMemoryChallengeStore implements ChallengeStore {
107
+ private readonly challenges;
108
+ set(challenge: AuthChallenge): Promise<void>;
109
+ consume(nonce: string): Promise<AuthChallenge | null>;
110
+ }
111
+ declare class InMemoryReplayStore implements ReplayStore {
112
+ private readonly seen;
113
+ has(key: string): Promise<boolean>;
114
+ put(key: string, expiresAt: number): Promise<void>;
115
+ private cleanup;
116
+ }
117
+ declare class InMemorySessionStore implements SessionStore {
118
+ private readonly sessions;
119
+ save(session: PlatformSession): Promise<void>;
120
+ get(sessionToken: string): Promise<PlatformSession | null>;
121
+ }
122
+
123
+ export { type AuthChallenge, type AuthCompleteInput, type AuthCompleteResult, type ChallengeStore, type EffectiveLevel, type IdentityMode, InMemoryChallengeStore, InMemoryReplayStore, InMemorySessionStore, type IngestEventsInput, type IngestEventsResult, type PlatformSession, type RarePlatformKit, type RarePlatformKitConfig, type ReplayStore, type SessionStore, type VerifiedActionContext, type VerifyActionInput, createRarePlatformKit };
package/dist/index.js ADDED
@@ -0,0 +1,296 @@
1
+ // src/index.ts
2
+ import {
3
+ buildActionPayload,
4
+ buildAuthChallengePayload,
5
+ generateNonce,
6
+ nowTs,
7
+ parseRareJwks,
8
+ signPlatformEventToken,
9
+ verifyDelegationToken,
10
+ verifyDetached,
11
+ verifyIdentityAttestation
12
+ } from "@rare-id/platform-kit-core";
13
+ function createRarePlatformKit(config) {
14
+ const challengeTtlSeconds = config.challengeTtlSeconds ?? 120;
15
+ const sessionTtlSeconds = config.sessionTtlSeconds ?? 3600;
16
+ const maxSignedTtlSeconds = config.maxSignedTtlSeconds ?? 300;
17
+ const clockSkewSeconds = config.clockSkewSeconds ?? 30;
18
+ const keyCache = config.initialJwks ? parseRareJwks(config.initialJwks) : {};
19
+ const resolveIdentityKey = async (kid) => {
20
+ if (config.keyResolver) {
21
+ return config.keyResolver(kid);
22
+ }
23
+ const existing = keyCache[kid];
24
+ if (existing) {
25
+ return existing;
26
+ }
27
+ if (!config.rareApiClient) {
28
+ return null;
29
+ }
30
+ const jwks = await config.rareApiClient.getJwks();
31
+ Object.assign(keyCache, parseRareJwks(jwks));
32
+ return keyCache[kid] ?? null;
33
+ };
34
+ return {
35
+ async issueChallenge(aud) {
36
+ const now = nowTs();
37
+ const challenge = {
38
+ nonce: generateNonce(18),
39
+ aud: aud ?? config.aud,
40
+ issuedAt: now,
41
+ expiresAt: now + challengeTtlSeconds
42
+ };
43
+ await config.challengeStore.set(challenge);
44
+ return challenge;
45
+ },
46
+ async completeAuth(input) {
47
+ const challenge = await config.challengeStore.consume(input.nonce);
48
+ if (!challenge) {
49
+ throw new Error("unknown challenge nonce");
50
+ }
51
+ const now = nowTs();
52
+ if (challenge.expiresAt < now - clockSkewSeconds) {
53
+ throw new Error("challenge expired");
54
+ }
55
+ const authPayload = buildAuthChallengePayload({
56
+ aud: challenge.aud,
57
+ nonce: challenge.nonce,
58
+ issuedAt: challenge.issuedAt,
59
+ expiresAt: challenge.expiresAt
60
+ });
61
+ if (!verifyDetached(
62
+ authPayload,
63
+ input.signatureBySession,
64
+ input.sessionPubkey
65
+ )) {
66
+ throw new Error("invalid session challenge signature");
67
+ }
68
+ const delegation = await verifyDelegationToken(input.delegationToken, {
69
+ expectedAud: config.aud,
70
+ requiredScope: "login",
71
+ rareSignerPublicKeyB64: config.rareSignerPublicKeyB64,
72
+ currentTs: now,
73
+ clockSkewSeconds
74
+ });
75
+ const delegationPayload = delegation.payload;
76
+ const delegatedSessionPubkey = delegationPayload.session_pubkey;
77
+ if (delegatedSessionPubkey !== input.sessionPubkey) {
78
+ throw new Error("session pubkey mismatch");
79
+ }
80
+ let identityMode = null;
81
+ let identityPayload = null;
82
+ if (input.fullIdentityAttestation) {
83
+ try {
84
+ const fullVerified = await verifyIdentityAttestation(
85
+ input.fullIdentityAttestation,
86
+ {
87
+ keyResolver: resolveIdentityKey,
88
+ expectedAud: config.aud,
89
+ currentTs: now,
90
+ clockSkewSeconds
91
+ }
92
+ );
93
+ identityMode = "full";
94
+ identityPayload = fullVerified.payload;
95
+ } catch {
96
+ }
97
+ }
98
+ if (!identityPayload && input.publicIdentityAttestation) {
99
+ const publicVerified = await verifyIdentityAttestation(
100
+ input.publicIdentityAttestation,
101
+ {
102
+ keyResolver: resolveIdentityKey,
103
+ currentTs: now,
104
+ clockSkewSeconds
105
+ }
106
+ );
107
+ identityMode = "public";
108
+ identityPayload = publicVerified.payload;
109
+ }
110
+ if (!identityPayload || !identityMode) {
111
+ throw new Error("missing identity attestation");
112
+ }
113
+ const delegatedAgent = delegationPayload.agent_id;
114
+ const identitySub = identityPayload.sub;
115
+ if (input.agentId !== delegatedAgent || input.agentId !== identitySub) {
116
+ throw new Error("agent identity triad mismatch");
117
+ }
118
+ const jti = delegationPayload.jti;
119
+ const exp = delegationPayload.exp;
120
+ if (typeof jti !== "string" || typeof exp !== "number") {
121
+ throw new Error("delegation replay fields missing");
122
+ }
123
+ const delegationReplayKey = `delegation:${jti}`;
124
+ if (await config.replayStore.has(delegationReplayKey)) {
125
+ throw new Error("delegation token replay detected");
126
+ }
127
+ await config.replayStore.put(delegationReplayKey, exp);
128
+ const rawLevel = identityPayload.lvl;
129
+ if (rawLevel !== "L0" && rawLevel !== "L1" && rawLevel !== "L2") {
130
+ throw new Error("unsupported identity level");
131
+ }
132
+ const effectiveLevel = identityMode === "public" && rawLevel === "L2" ? "L1" : rawLevel;
133
+ let displayName = "unknown";
134
+ const claims = identityPayload.claims;
135
+ if (claims && typeof claims === "object") {
136
+ const profile = claims.profile;
137
+ if (profile && typeof profile === "object") {
138
+ const maybeName = profile.name;
139
+ if (typeof maybeName === "string" && maybeName.trim()) {
140
+ displayName = maybeName;
141
+ }
142
+ }
143
+ }
144
+ const sessionToken = generateNonce(24);
145
+ const session = {
146
+ sessionToken,
147
+ agentId: input.agentId,
148
+ sessionPubkey: input.sessionPubkey,
149
+ identityMode,
150
+ rawLevel,
151
+ effectiveLevel,
152
+ displayName,
153
+ aud: config.aud,
154
+ createdAt: now,
155
+ expiresAt: now + sessionTtlSeconds
156
+ };
157
+ await config.sessionStore.save(session);
158
+ return {
159
+ session_token: session.sessionToken,
160
+ agent_id: session.agentId,
161
+ level: session.effectiveLevel,
162
+ raw_level: session.rawLevel,
163
+ identity_mode: session.identityMode,
164
+ display_name: session.displayName,
165
+ session_pubkey: session.sessionPubkey
166
+ };
167
+ },
168
+ async verifyAction(input) {
169
+ const session = await config.sessionStore.get(input.sessionToken);
170
+ if (!session) {
171
+ throw new Error("invalid session token");
172
+ }
173
+ const now = nowTs();
174
+ if (session.expiresAt < now) {
175
+ throw new Error("session expired");
176
+ }
177
+ if (input.issuedAt > now + clockSkewSeconds) {
178
+ throw new Error("action issued_at too far in future");
179
+ }
180
+ if (input.expiresAt < now - clockSkewSeconds) {
181
+ throw new Error("action expired");
182
+ }
183
+ if (input.expiresAt <= input.issuedAt) {
184
+ throw new Error("action expires_at must be greater than issued_at");
185
+ }
186
+ if (input.expiresAt - input.issuedAt > maxSignedTtlSeconds) {
187
+ throw new Error(
188
+ `action ttl exceeds max ${maxSignedTtlSeconds} seconds`
189
+ );
190
+ }
191
+ const replayKey = `action:${session.sessionToken}:${input.nonce}`;
192
+ if (await config.replayStore.has(replayKey)) {
193
+ throw new Error("action nonce already consumed");
194
+ }
195
+ await config.replayStore.put(replayKey, input.expiresAt);
196
+ const signingInput = buildActionPayload({
197
+ aud: config.aud,
198
+ sessionToken: session.sessionToken,
199
+ action: input.action,
200
+ actionPayload: input.actionPayload,
201
+ nonce: input.nonce,
202
+ issuedAt: input.issuedAt,
203
+ expiresAt: input.expiresAt
204
+ });
205
+ if (!verifyDetached(
206
+ signingInput,
207
+ input.signatureBySession,
208
+ session.sessionPubkey
209
+ )) {
210
+ throw new Error("invalid action signature");
211
+ }
212
+ return {
213
+ session,
214
+ action: input.action,
215
+ actionPayload: input.actionPayload
216
+ };
217
+ },
218
+ async ingestNegativeEvents(input) {
219
+ if (!config.rareApiClient) {
220
+ throw new Error("rareApiClient is required for event ingest");
221
+ }
222
+ let eventToken = input.eventToken;
223
+ if (!eventToken) {
224
+ if (!input.platformId || !input.kid || !input.privateKeyPem || !input.jti || !input.events) {
225
+ throw new Error("missing event signing input");
226
+ }
227
+ eventToken = await signPlatformEventToken({
228
+ platformId: input.platformId,
229
+ kid: input.kid,
230
+ privateKeyPem: input.privateKeyPem,
231
+ jti: input.jti,
232
+ events: input.events,
233
+ issuedAt: input.issuedAt,
234
+ expiresAt: input.expiresAt
235
+ });
236
+ }
237
+ const response = await config.rareApiClient.ingestPlatformEvents(eventToken);
238
+ return { eventToken, response };
239
+ }
240
+ };
241
+ }
242
+ var InMemoryChallengeStore = class {
243
+ challenges = /* @__PURE__ */ new Map();
244
+ async set(challenge) {
245
+ this.challenges.set(challenge.nonce, challenge);
246
+ }
247
+ async consume(nonce) {
248
+ const challenge = this.challenges.get(nonce) ?? null;
249
+ if (challenge) {
250
+ this.challenges.delete(nonce);
251
+ }
252
+ return challenge;
253
+ }
254
+ };
255
+ var InMemoryReplayStore = class {
256
+ seen = /* @__PURE__ */ new Map();
257
+ async has(key) {
258
+ this.cleanup();
259
+ return this.seen.has(key);
260
+ }
261
+ async put(key, expiresAt) {
262
+ this.seen.set(key, expiresAt);
263
+ this.cleanup();
264
+ }
265
+ cleanup() {
266
+ const now = nowTs();
267
+ for (const [key, expiresAt] of this.seen.entries()) {
268
+ if (expiresAt < now) {
269
+ this.seen.delete(key);
270
+ }
271
+ }
272
+ }
273
+ };
274
+ var InMemorySessionStore = class {
275
+ sessions = /* @__PURE__ */ new Map();
276
+ async save(session) {
277
+ this.sessions.set(session.sessionToken, session);
278
+ }
279
+ async get(sessionToken) {
280
+ const session = this.sessions.get(sessionToken) ?? null;
281
+ if (!session) {
282
+ return null;
283
+ }
284
+ if (session.expiresAt < nowTs()) {
285
+ this.sessions.delete(sessionToken);
286
+ return null;
287
+ }
288
+ return session;
289
+ }
290
+ };
291
+ export {
292
+ InMemoryChallengeStore,
293
+ InMemoryReplayStore,
294
+ InMemorySessionStore,
295
+ createRarePlatformKit
296
+ };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@rare-id/platform-kit-web",
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-web"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public",
25
+ "provenance": false
26
+ },
27
+ "dependencies": {
28
+ "@rare-id/platform-kit-client": "0.1.0",
29
+ "@rare-id/platform-kit-core": "0.1.0"
30
+ },
31
+ "devDependencies": {
32
+ "jose": "^6.0.8",
33
+ "tweetnacl": "^1.0.3"
34
+ },
35
+ "scripts": {
36
+ "build": "tsup src/index.ts --format esm --dts",
37
+ "test": "vitest run",
38
+ "lint": "biome check src test",
39
+ "typecheck": "tsc -p tsconfig.json --noEmit"
40
+ }
41
+ }