@rakomi/node 0.0.0 → 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/LICENSE +21 -0
- package/README.md +57 -1
- package/SECURITY.md +206 -0
- package/dist/agents.d.ts +90 -0
- package/dist/agents.js +203 -0
- package/dist/anonymous.d.ts +50 -0
- package/dist/anonymous.js +105 -0
- package/dist/ciba.d.ts +97 -0
- package/dist/ciba.js +282 -0
- package/dist/client.d.ts +93 -0
- package/dist/client.js +202 -0
- package/dist/credentials.d.ts +87 -0
- package/dist/credentials.js +104 -0
- package/dist/device.d.ts +76 -0
- package/dist/device.js +244 -0
- package/dist/doctor.d.ts +11 -0
- package/dist/doctor.js +135 -0
- package/dist/dpop-session.d.ts +90 -0
- package/dist/dpop-session.js +127 -0
- package/dist/dpop.d.ts +24 -0
- package/dist/dpop.js +51 -0
- package/dist/env-detect.d.ts +11 -0
- package/dist/env-detect.js +26 -0
- package/dist/errors.d.ts +307 -0
- package/dist/errors.js +385 -0
- package/dist/eudi.d.ts +23 -0
- package/dist/eudi.js +27 -0
- package/dist/flags.d.ts +50 -0
- package/dist/flags.js +173 -0
- package/dist/guards.d.ts +16 -0
- package/dist/guards.js +104 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +18 -0
- package/dist/internal/canonical-url.d.ts +13 -0
- package/dist/internal/canonical-url.js +52 -0
- package/dist/internal/shared-constants.d.ts +3 -0
- package/dist/internal/shared-constants.js +3 -0
- package/dist/jwks-cache.d.ts +31 -0
- package/dist/jwks-cache.js +135 -0
- package/dist/link.d.ts +73 -0
- package/dist/link.js +262 -0
- package/dist/middleware.d.ts +21 -0
- package/dist/middleware.js +84 -0
- package/dist/oauth.d.ts +46 -0
- package/dist/oauth.js +457 -0
- package/dist/rbac.d.ts +12 -0
- package/dist/rbac.js +20 -0
- package/dist/token-exchange.d.ts +65 -0
- package/dist/token-exchange.js +163 -0
- package/dist/types.d.ts +436 -0
- package/dist/types.js +1 -0
- package/dist/verify-publisher-webhook.d.ts +25 -0
- package/dist/verify-publisher-webhook.js +47 -0
- package/dist/verify-token.d.ts +3 -0
- package/dist/verify-token.js +148 -0
- package/dist/verify-webhook.d.ts +7 -0
- package/dist/verify-webhook.js +101 -0
- package/package.json +61 -5
- package/sbom.cdx.json +52 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { decodeProtectedHeader, errors as joseErrors, jwtVerify } from 'jose';
|
|
2
|
+
import { AUTH_ENVIRONMENT_MISMATCH, TOKEN_EXPIRED, TOKEN_INVALID_ALGORITHM, TOKEN_INVALID_AUDIENCE, TOKEN_INVALID_ISSUER, TOKEN_INVALID_SIGNATURE, TOKEN_MALFORMED, TOKEN_MISSING_CLAIMS, TOKEN_NOT_YET_VALID, TOKEN_REVOKED, } from './errors.js';
|
|
3
|
+
const ISSUER = 'https://rakomi.com';
|
|
4
|
+
const AUDIENCE = 'https://rakomi.com';
|
|
5
|
+
const ALLOWED_ALGORITHMS = ['RS256'];
|
|
6
|
+
const REQUIRED_CLAIMS = ['sub', 'tenant_id', 'iss', 'aud', 'exp', 'iat', 'jti'];
|
|
7
|
+
const USER_REQUIRED_CLAIMS = ['email', 'sid'];
|
|
8
|
+
export async function verifyToken(token, jwksCache, clockTolerance, sdkEnvironment) {
|
|
9
|
+
let kid;
|
|
10
|
+
try {
|
|
11
|
+
const header = decodeProtectedHeader(token);
|
|
12
|
+
if (header.alg !== 'RS256') {
|
|
13
|
+
return { ok: false, error: TOKEN_INVALID_ALGORITHM() };
|
|
14
|
+
}
|
|
15
|
+
kid = header.kid;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return { ok: false, error: TOKEN_MALFORMED() };
|
|
19
|
+
}
|
|
20
|
+
if (!kid) {
|
|
21
|
+
return { ok: false, error: TOKEN_MALFORMED() };
|
|
22
|
+
}
|
|
23
|
+
const keyResult = await jwksCache.getKey(kid);
|
|
24
|
+
if (!keyResult.ok) {
|
|
25
|
+
return keyResult;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const { payload } = await jwtVerify(token, keyResult.data, {
|
|
29
|
+
algorithms: [...ALLOWED_ALGORITHMS],
|
|
30
|
+
issuer: ISSUER,
|
|
31
|
+
audience: AUDIENCE,
|
|
32
|
+
clockTolerance,
|
|
33
|
+
maxTokenAge: '3660s',
|
|
34
|
+
});
|
|
35
|
+
if (Array.isArray(payload.aud)) {
|
|
36
|
+
return { ok: false, error: TOKEN_INVALID_AUDIENCE() };
|
|
37
|
+
}
|
|
38
|
+
for (const claim of REQUIRED_CLAIMS) {
|
|
39
|
+
if (payload[claim] === undefined || payload[claim] === null) {
|
|
40
|
+
return { ok: false, error: TOKEN_MISSING_CLAIMS() };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const isM2MToken = payload.m2m === true;
|
|
44
|
+
if (!isM2MToken) {
|
|
45
|
+
for (const claim of USER_REQUIRED_CLAIMS) {
|
|
46
|
+
if (payload[claim] === undefined || payload[claim] === null) {
|
|
47
|
+
return { ok: false, error: TOKEN_MISSING_CLAIMS() };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const epoch = jwksCache.getRevocationEpoch();
|
|
52
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
53
|
+
if (epoch && epoch > 0 && epoch <= nowSeconds) {
|
|
54
|
+
if (payload.iat === undefined || payload.iat < epoch) {
|
|
55
|
+
return { ok: false, error: TOKEN_REVOKED() };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (sdkEnvironment && payload.rkm_env) {
|
|
59
|
+
const tokenEnv = payload.rkm_env;
|
|
60
|
+
const sdkIsLive = sdkEnvironment === 'live';
|
|
61
|
+
const tokenIsLive = tokenEnv === 'live';
|
|
62
|
+
if (sdkIsLive !== tokenIsLive) {
|
|
63
|
+
return { ok: false, error: AUTH_ENVIRONMENT_MISMATCH() };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const mapped = {
|
|
67
|
+
userId: payload.sub,
|
|
68
|
+
...(payload.email ? { email: payload.email } : {}),
|
|
69
|
+
tenantId: payload.tenant_id,
|
|
70
|
+
...(payload.sid ? { sessionId: payload.sid } : {}),
|
|
71
|
+
iss: payload.iss,
|
|
72
|
+
aud: (Array.isArray(payload.aud) ? payload.aud[0] : payload.aud),
|
|
73
|
+
exp: payload.exp,
|
|
74
|
+
iat: payload.iat,
|
|
75
|
+
jti: payload.jti,
|
|
76
|
+
...(payload.mfa_verified === true ? {
|
|
77
|
+
mfaVerified: true,
|
|
78
|
+
mfaVerifiedAt: payload.mfa_verified_at,
|
|
79
|
+
} : {}),
|
|
80
|
+
...(payload.amr ? { amr: payload.amr } : {}),
|
|
81
|
+
...(payload.acr ? { acr: payload.acr } : {}),
|
|
82
|
+
...(payload.auth_time != null ? { authTime: payload.auth_time } : {}),
|
|
83
|
+
roles: payload.roles ?? [],
|
|
84
|
+
permissions: payload.permissions ?? [],
|
|
85
|
+
...(payload.rkm_env ? { environment: payload.rkm_env } : {}),
|
|
86
|
+
...(payload.public_metadata ? { publicMetadata: payload.public_metadata } : {}),
|
|
87
|
+
...(typeof payload.is_minor === 'boolean' ? { isMinor: payload.is_minor } : {}),
|
|
88
|
+
...(payload.subscription ? { subscription: payload.subscription } : {}),
|
|
89
|
+
...(payload.m2m === true ? { isM2M: true } : {}),
|
|
90
|
+
...(payload.client_id ? { clientId: payload.client_id } : {}),
|
|
91
|
+
...(payload.scope ? { scopes: payload.scope.split(' ').filter(Boolean) } : {}),
|
|
92
|
+
};
|
|
93
|
+
const actClaim = payload.act;
|
|
94
|
+
if (actClaim && typeof actClaim === 'object' && typeof actClaim.sub === 'string') {
|
|
95
|
+
mapped.isAgentToken = true;
|
|
96
|
+
const tokenScopes = typeof payload.scope === 'string'
|
|
97
|
+
? payload.scope.split(' ').filter(Boolean)
|
|
98
|
+
: [];
|
|
99
|
+
mapped.agent = { clientId: actClaim.sub, scopes: tokenScopes };
|
|
100
|
+
}
|
|
101
|
+
if (!mapped.isM2M) {
|
|
102
|
+
const nowSecondsForMeta = Math.floor(Date.now() / 1000);
|
|
103
|
+
const expiresIn = Math.max(0, payload.exp - nowSecondsForMeta);
|
|
104
|
+
const tokenMeta = { expiresIn };
|
|
105
|
+
const session = {
|
|
106
|
+
expiresAt: new Date(payload.exp * 1000).toISOString(),
|
|
107
|
+
isExpiringSoon: false,
|
|
108
|
+
};
|
|
109
|
+
if (typeof payload.session_max_lifetime_exp === 'number') {
|
|
110
|
+
session.maxLifetimeExpiresAt = new Date(payload.session_max_lifetime_exp * 1000).toISOString();
|
|
111
|
+
}
|
|
112
|
+
const maxLifeRemaining = typeof payload.session_max_lifetime_exp === 'number'
|
|
113
|
+
? Math.max(0, payload.session_max_lifetime_exp - nowSecondsForMeta)
|
|
114
|
+
: Infinity;
|
|
115
|
+
session.isExpiringSoon = Math.min(tokenMeta.expiresIn, maxLifeRemaining) < 300;
|
|
116
|
+
mapped.session = session;
|
|
117
|
+
mapped.token = tokenMeta;
|
|
118
|
+
}
|
|
119
|
+
return { ok: true, data: mapped };
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
return { ok: false, error: mapJoseError(err) };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function mapJoseError(err) {
|
|
126
|
+
if (err instanceof joseErrors.JWTExpired) {
|
|
127
|
+
return TOKEN_EXPIRED();
|
|
128
|
+
}
|
|
129
|
+
if (err instanceof joseErrors.JWSSignatureVerificationFailed) {
|
|
130
|
+
return TOKEN_INVALID_SIGNATURE();
|
|
131
|
+
}
|
|
132
|
+
if (err instanceof joseErrors.JWTClaimValidationFailed) {
|
|
133
|
+
if (err.claim === 'iss') {
|
|
134
|
+
return TOKEN_INVALID_ISSUER();
|
|
135
|
+
}
|
|
136
|
+
if (err.claim === 'aud') {
|
|
137
|
+
return TOKEN_INVALID_AUDIENCE();
|
|
138
|
+
}
|
|
139
|
+
if (err.claim === 'nbf') {
|
|
140
|
+
return TOKEN_NOT_YET_VALID();
|
|
141
|
+
}
|
|
142
|
+
return TOKEN_MISSING_CLAIMS();
|
|
143
|
+
}
|
|
144
|
+
if (err instanceof joseErrors.JOSEError) {
|
|
145
|
+
return TOKEN_MALFORMED();
|
|
146
|
+
}
|
|
147
|
+
return TOKEN_MALFORMED();
|
|
148
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { VerifyResult, WebhookEvent, WebhookVerifyData } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Case-insensitive header lookup (RFC 9110 §5.1). Exported so the publisher wrapper can read the
|
|
4
|
+
* `X-Rakomi-Event` header with the identical lookup semantics (no second header parser to drift).
|
|
5
|
+
*/
|
|
6
|
+
export declare function getHeader(headers: Record<string, string | string[] | undefined>, name: string): string | undefined;
|
|
7
|
+
export declare function verifyWebhook<T = WebhookEvent>(body: string | Buffer, headers: Record<string, string | string[] | undefined>, secret: string, tolerance: number): Promise<VerifyResult<WebhookVerifyData<T>>>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { WEBHOOK_INVALID_BODY, WEBHOOK_INVALID_SECRET, WEBHOOK_INVALID_SIGNATURE, WEBHOOK_MISSING_HEADER, WEBHOOK_TIMESTAMP_TOO_NEW, WEBHOOK_TIMESTAMP_TOO_OLD, } from './errors.js';
|
|
3
|
+
const RKSEC_PREFIX = 'rksec_';
|
|
4
|
+
const EXPECTED_KEY_LENGTH = 32;
|
|
5
|
+
/**
|
|
6
|
+
* Case-insensitive header lookup (RFC 9110 §5.1). Exported so the publisher wrapper can read the
|
|
7
|
+
* `X-Rakomi-Event` header with the identical lookup semantics (no second header parser to drift).
|
|
8
|
+
*/
|
|
9
|
+
export function getHeader(headers, name) {
|
|
10
|
+
const lower = name.toLowerCase();
|
|
11
|
+
let value = headers[lower];
|
|
12
|
+
if (value === undefined) {
|
|
13
|
+
for (const key of Object.keys(headers)) {
|
|
14
|
+
if (key.toLowerCase() === lower) {
|
|
15
|
+
value = headers[key];
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (Array.isArray(value))
|
|
21
|
+
return value[0]?.trim();
|
|
22
|
+
return value?.trim();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Derive the raw HMAC key from a webhook signing secret.
|
|
26
|
+
* Prefixed secrets (`rksec_<base64url>`) are stripped and base64url-decoded
|
|
27
|
+
* to recover the original 32-byte key. Plain strings are used as-is.
|
|
28
|
+
* Validates decoded key is exactly 32 bytes.
|
|
29
|
+
*/
|
|
30
|
+
function deriveKey(secret) {
|
|
31
|
+
if (secret.startsWith(RKSEC_PREFIX)) {
|
|
32
|
+
const key = Buffer.from(secret.slice(RKSEC_PREFIX.length), 'base64url');
|
|
33
|
+
if (key.length !== EXPECTED_KEY_LENGTH) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return key;
|
|
37
|
+
}
|
|
38
|
+
return Buffer.from(secret, 'utf8');
|
|
39
|
+
}
|
|
40
|
+
export async function verifyWebhook(body, headers, secret, tolerance) {
|
|
41
|
+
try {
|
|
42
|
+
const signature = getHeader(headers, 'webhook-signature');
|
|
43
|
+
const timestampStr = getHeader(headers, 'webhook-timestamp');
|
|
44
|
+
const webhookId = getHeader(headers, 'webhook-id');
|
|
45
|
+
const deliveryId = getHeader(headers, 'x-rakomi-delivery-id');
|
|
46
|
+
if (!signature || !timestampStr || !webhookId) {
|
|
47
|
+
return { ok: false, error: WEBHOOK_MISSING_HEADER() };
|
|
48
|
+
}
|
|
49
|
+
const timestamp = parseInt(timestampStr, 10);
|
|
50
|
+
if (isNaN(timestamp)) {
|
|
51
|
+
return { ok: false, error: WEBHOOK_MISSING_HEADER() };
|
|
52
|
+
}
|
|
53
|
+
const now = Math.floor(Date.now() / 1000);
|
|
54
|
+
const diff = now - timestamp;
|
|
55
|
+
if (diff > tolerance) {
|
|
56
|
+
return { ok: false, error: WEBHOOK_TIMESTAMP_TOO_OLD(tolerance) };
|
|
57
|
+
}
|
|
58
|
+
if (diff < -tolerance) {
|
|
59
|
+
return { ok: false, error: WEBHOOK_TIMESTAMP_TOO_NEW(tolerance) };
|
|
60
|
+
}
|
|
61
|
+
const key = deriveKey(secret);
|
|
62
|
+
if (!key) {
|
|
63
|
+
return { ok: false, error: WEBHOOK_INVALID_SECRET() };
|
|
64
|
+
}
|
|
65
|
+
const bodyString = typeof body === 'string' ? body : body.toString('utf-8');
|
|
66
|
+
const signedContent = `${webhookId}.${timestampStr}.${bodyString}`;
|
|
67
|
+
const expectedSig = createHmac('sha256', key)
|
|
68
|
+
.update(signedContent)
|
|
69
|
+
.digest('base64');
|
|
70
|
+
const signatureEntries = signature.split(' ').map((s) => s.trim()).filter(Boolean);
|
|
71
|
+
let matched = false;
|
|
72
|
+
for (const entry of signatureEntries) {
|
|
73
|
+
if (!entry.startsWith('v1,'))
|
|
74
|
+
continue;
|
|
75
|
+
const receivedSig = entry.slice(3);
|
|
76
|
+
try {
|
|
77
|
+
const expectedRaw = Buffer.from(expectedSig, 'base64');
|
|
78
|
+
const receivedRaw = Buffer.from(receivedSig, 'base64');
|
|
79
|
+
if (expectedRaw.length === 32 &&
|
|
80
|
+
receivedRaw.length === 32 &&
|
|
81
|
+
timingSafeEqual(expectedRaw, receivedRaw)) {
|
|
82
|
+
matched = true;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!matched) {
|
|
90
|
+
return { ok: false, error: WEBHOOK_INVALID_SIGNATURE() };
|
|
91
|
+
}
|
|
92
|
+
const parsed = JSON.parse(bodyString);
|
|
93
|
+
return {
|
|
94
|
+
ok: true,
|
|
95
|
+
data: { webhookId, deliveryId: deliveryId ?? webhookId, timestamp, payload: parsed },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return { ok: false, error: WEBHOOK_INVALID_BODY() };
|
|
100
|
+
}
|
|
101
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rakomi/node",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Server-side Node.js SDK for Rakomi — verify access tokens and webhook signatures. EU-native auth-as-a-service.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"rakomi",
|
|
7
|
+
"authentication",
|
|
8
|
+
"auth",
|
|
9
|
+
"oauth",
|
|
10
|
+
"oidc",
|
|
11
|
+
"jwt",
|
|
12
|
+
"jwks",
|
|
13
|
+
"webhook",
|
|
14
|
+
"sdk",
|
|
15
|
+
"nodejs"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/rakomidev/rakomi-js.git"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/rakomidev/rakomi-js/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/rakomidev/rakomi-js#readme",
|
|
25
|
+
"rakomi": {
|
|
26
|
+
"apiVersion": "2026-03-01"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"provenance": true
|
|
30
|
+
},
|
|
31
|
+
"type": "module",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"sideEffects": false,
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"import": "./dist/index.js"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"bin": {
|
|
41
|
+
"rakomi-doctor": "./dist/doctor.js"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=22"
|
|
45
|
+
},
|
|
46
|
+
"author": "CRE8EVE Sp. z o.o.",
|
|
47
|
+
"files": [
|
|
48
|
+
"dist",
|
|
49
|
+
"README.md",
|
|
50
|
+
"LICENSE",
|
|
51
|
+
"SECURITY.md",
|
|
52
|
+
"sbom.cdx.json"
|
|
53
|
+
],
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"jose": "^6.2.3"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsc",
|
|
59
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.typetest.json",
|
|
60
|
+
"lint": "eslint src/ test/ --max-warnings=0",
|
|
61
|
+
"test": "vitest run"
|
|
62
|
+
}
|
|
63
|
+
}
|
package/sbom.cdx.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bomFormat": "CycloneDX",
|
|
3
|
+
"specVersion": "1.6",
|
|
4
|
+
"version": 1,
|
|
5
|
+
"metadata": {
|
|
6
|
+
"authors": [
|
|
7
|
+
{
|
|
8
|
+
"name": "CRE8EVE"
|
|
9
|
+
}
|
|
10
|
+
],
|
|
11
|
+
"tools": {
|
|
12
|
+
"components": [
|
|
13
|
+
{
|
|
14
|
+
"type": "application",
|
|
15
|
+
"group": "rakomi",
|
|
16
|
+
"name": "generate-sbom",
|
|
17
|
+
"version": "sha256:6612f58a07b8"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"component": {
|
|
22
|
+
"type": "library",
|
|
23
|
+
"bom-ref": "pkg:npm/%40rakomi%2Fnode@0.1.0",
|
|
24
|
+
"name": "@rakomi/node",
|
|
25
|
+
"version": "0.1.0",
|
|
26
|
+
"purl": "pkg:npm/%40rakomi%2Fnode@0.1.0",
|
|
27
|
+
"licenses": [
|
|
28
|
+
{
|
|
29
|
+
"license": {
|
|
30
|
+
"id": "MIT"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"components": [
|
|
37
|
+
{
|
|
38
|
+
"type": "library",
|
|
39
|
+
"bom-ref": "pkg:npm/jose@6.2.3",
|
|
40
|
+
"name": "jose",
|
|
41
|
+
"version": "6.2.3",
|
|
42
|
+
"purl": "pkg:npm/jose@6.2.3",
|
|
43
|
+
"licenses": [
|
|
44
|
+
{
|
|
45
|
+
"license": {
|
|
46
|
+
"id": "MIT"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|