From 4558b9ecb9e986ac5095638c14877fbbf8488e8d Mon Sep 17 00:00:00 2001 From: Sergii Date: Tue, 7 Oct 2025 05:27:59 +0300 Subject: [PATCH] feat: expose jwt generation for managed wallets --- .husky/pre-commit | 3 - apps/api/package.json | 2 +- .../jwt-token/jwt-token.controller.spec.ts | 86 +++ .../jwt-token/jwt-token.controller.ts | 33 + .../provider/http-schemas/jwt-token.schema.ts | 56 ++ .../src/provider/providers/jwt.provider.ts | 7 +- apps/api/src/provider/routes/index.ts | 1 + .../routes/jwt-token/jwt-token.router.ts | 46 ++ .../provider-jwt-token.service.spec.ts | 14 +- .../provider-jwt-token.service.ts | 16 +- .../provider/provider.service.spec.ts | 2 +- apps/api/src/rest-app.ts | 2 + .../__snapshots__/docs.spec.ts.snap | 238 +++++++ apps/provider-proxy/package.json | 4 +- apps/provider-proxy/src/utils/schema.ts | 4 +- .../functional/provider-proxy-http.spec.ts | 6 +- .../test/functional/provider-proxy-ws.spec.ts | 6 +- apps/provider-proxy/tsconfig.build.json | 3 +- package-lock.json | 343 +++++++++- packages/jwt/.prettierrc.js | 1 - packages/jwt/jest.config.ts | 8 - packages/jwt/package.json | 38 -- packages/jwt/scripts/generate.ts | 70 -- packages/jwt/src/base64.ts | 14 - packages/jwt/src/generated/jwt-schema-data.ts | 284 -------- packages/jwt/src/index.ts | 4 - packages/jwt/src/jwt-token.spec.ts | 69 -- packages/jwt/src/jwt-token.ts | 122 ---- packages/jwt/src/jwt-validator.spec.ts | 61 -- packages/jwt/src/jwt-validator.ts | 234 ------- .../test/generated/jwt-claims-test-cases.ts | 626 ------------------ .../jwt/src/test/generated/jwt-mnemonic.ts | 3 - .../test/generated/jwt-signing-test-cases.ts | 30 - .../src/test/seeders/akash-address.seeder.ts | 5 - packages/jwt/src/test/test-utils.ts | 49 -- packages/jwt/src/types.ts | 67 -- packages/jwt/src/wallet-utils.ts | 45 -- packages/jwt/tsconfig.build.json | 10 - packages/jwt/tsconfig.json | 5 - 39 files changed, 830 insertions(+), 1787 deletions(-) create mode 100644 apps/api/src/provider/controllers/jwt-token/jwt-token.controller.spec.ts create mode 100644 apps/api/src/provider/controllers/jwt-token/jwt-token.controller.ts create mode 100644 apps/api/src/provider/http-schemas/jwt-token.schema.ts create mode 100644 apps/api/src/provider/routes/jwt-token/jwt-token.router.ts delete mode 100644 packages/jwt/.prettierrc.js delete mode 100644 packages/jwt/jest.config.ts delete mode 100644 packages/jwt/package.json delete mode 100644 packages/jwt/scripts/generate.ts delete mode 100644 packages/jwt/src/base64.ts delete mode 100644 packages/jwt/src/generated/jwt-schema-data.ts delete mode 100644 packages/jwt/src/index.ts delete mode 100644 packages/jwt/src/jwt-token.spec.ts delete mode 100644 packages/jwt/src/jwt-token.ts delete mode 100644 packages/jwt/src/jwt-validator.spec.ts delete mode 100644 packages/jwt/src/jwt-validator.ts delete mode 100644 packages/jwt/src/test/generated/jwt-claims-test-cases.ts delete mode 100644 packages/jwt/src/test/generated/jwt-mnemonic.ts delete mode 100644 packages/jwt/src/test/generated/jwt-signing-test-cases.ts delete mode 100644 packages/jwt/src/test/seeders/akash-address.seeder.ts delete mode 100644 packages/jwt/src/test/test-utils.ts delete mode 100644 packages/jwt/src/types.ts delete mode 100644 packages/jwt/src/wallet-utils.ts delete mode 100644 packages/jwt/tsconfig.build.json delete mode 100644 packages/jwt/tsconfig.json diff --git a/.husky/pre-commit b/.husky/pre-commit index e3a56a4db..6e1b9b6d5 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -4,9 +4,6 @@ if [[ "$CI" != "true" ]]; then npm run generate -w packages/net git add ./packages/net/src/generated - npm run generate -w packages/jwt - git add ./packages/jwt/src/generated - echo "" echo "Checking if package-lock.json and package.json are in sync..." echo "" diff --git a/apps/api/package.json b/apps/api/package.json index 3b0400537..9c484ec2d 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -39,10 +39,10 @@ "dependencies": { "@akashnetwork/akash-api": "^1.3.0", "@akashnetwork/akashjs": "^0.11.1", + "@akashnetwork/chain-sdk": "^1.0.0-alpha.3", "@akashnetwork/database": "*", "@akashnetwork/env-loader": "*", "@akashnetwork/http-sdk": "*", - "@akashnetwork/jwt": "*", "@akashnetwork/logging": "*", "@akashnetwork/net": "*", "@akashnetwork/react-query-sdk": "*", diff --git a/apps/api/src/provider/controllers/jwt-token/jwt-token.controller.spec.ts b/apps/api/src/provider/controllers/jwt-token/jwt-token.controller.spec.ts new file mode 100644 index 000000000..4819ac88e --- /dev/null +++ b/apps/api/src/provider/controllers/jwt-token/jwt-token.controller.spec.ts @@ -0,0 +1,86 @@ +import { faker } from "@faker-js/faker"; +import { mock } from "jest-mock-extended"; + +import { UserSeeder } from "../../../../test/seeders/user.seeder"; +import { UserWalletSeeder } from "../../../../test/seeders/user-wallet.seeder"; +import type { AuthService } from "../../../auth/services/auth.service"; +import type { UserWalletRepository } from "../../../billing/repositories"; +import type { UserOutput } from "../../../user/repositories/user/user.repository"; +import type { CreateJwtTokenRequest } from "../../http-schemas/jwt-token.schema"; +import type { ProviderJwtTokenService } from "../../services/provider-jwt-token/provider-jwt-token.service"; +import { JwtTokenController } from "./jwt-token.controller"; + +describe(JwtTokenController.name, () => { + describe("createJwtToken", () => { + it("creates JWT token successfully when user has wallet", async () => { + const user = UserSeeder.create(); + const { controller, authService, userWalletRepository, providerJwtTokenService, jwtToken, wallet } = setup({ user }); + + authService.currentUser = user; + userWalletRepository.accessibleBy.mockReturnThis(); + userWalletRepository.findOneByUserId.mockResolvedValue(wallet); + providerJwtTokenService.generateJwtToken.mockResolvedValue(jwtToken); + + const payload = createPayload(); + const result = await controller.createJwtToken(payload); + + expect(result).toEqual({ token: jwtToken }); + expect(userWalletRepository.accessibleBy).toHaveBeenCalledWith(authService.ability, "sign"); + expect(userWalletRepository.findOneByUserId).toHaveBeenCalledWith(user.id); + expect(providerJwtTokenService.generateJwtToken).toHaveBeenCalledWith({ + walletId: wallet.id, + leases: payload.leases, + ttl: payload.ttl + }); + }); + + it("throws 401 when user is not authenticated", async () => { + const { controller, authService } = setup(); + + authService.currentUser = undefined as any; + + await expect(controller.createJwtToken(createPayload())).rejects.toThrow("Unauthorized"); + }); + + it("throws 400 when user has no wallet", async () => { + const user = UserSeeder.create(); + const { controller, authService, userWalletRepository } = setup({ user }); + + authService.currentUser = user; + userWalletRepository.accessibleBy.mockReturnThis(); + userWalletRepository.findOneByUserId.mockResolvedValue(undefined); + + await expect(controller.createJwtToken(createPayload())).rejects.toThrow("User does not have a wallet"); + }); + }); + + function setup(input?: { user?: UserOutput }) { + const authService = mock(); + const userWalletRepository = mock(); + const providerJwtTokenService = mock(); + + const controller = new JwtTokenController(providerJwtTokenService, authService, userWalletRepository); + + const wallet = UserWalletSeeder.create({ userId: input?.user?.id }); + const jwtToken = faker.string.alphanumeric(64); + + return { + controller, + authService, + userWalletRepository, + providerJwtTokenService, + jwtToken, + wallet + }; + } + + function createPayload(): CreateJwtTokenRequest { + return { + ttl: faker.number.int({ min: 3600, max: 86400 }), + leases: { + access: "full", + scope: ["send-manifest", "get-manifest"] + } + }; + } +}); diff --git a/apps/api/src/provider/controllers/jwt-token/jwt-token.controller.ts b/apps/api/src/provider/controllers/jwt-token/jwt-token.controller.ts new file mode 100644 index 000000000..178e3481c --- /dev/null +++ b/apps/api/src/provider/controllers/jwt-token/jwt-token.controller.ts @@ -0,0 +1,33 @@ +import assert from "http-assert"; +import { singleton } from "tsyringe"; + +import { AuthService } from "@src/auth/services/auth.service"; +import { UserWalletRepository } from "@src/billing/repositories"; +import { CreateJwtTokenRequest, CreateJwtTokenResponse } from "../../http-schemas/jwt-token.schema"; +import { ProviderJwtTokenService } from "../../services/provider-jwt-token/provider-jwt-token.service"; + +@singleton() +export class JwtTokenController { + constructor( + private readonly providerJwtTokenService: ProviderJwtTokenService, + private readonly authService: AuthService, + private readonly userWalletRepository: UserWalletRepository + ) {} + + async createJwtToken(payload: CreateJwtTokenRequest): Promise { + assert(this.authService.currentUser, 401); + + const wallet = await this.userWalletRepository.accessibleBy(this.authService.ability, "sign").findOneByUserId(this.authService.currentUser.id); + assert(wallet, 400, "User does not have a wallet"); + + const jwtToken = await this.providerJwtTokenService.generateJwtToken({ + walletId: wallet.id, + leases: payload.leases, + ttl: payload.ttl + }); + + return { + token: jwtToken + }; + } +} diff --git a/apps/api/src/provider/http-schemas/jwt-token.schema.ts b/apps/api/src/provider/http-schemas/jwt-token.schema.ts new file mode 100644 index 000000000..635e4e39d --- /dev/null +++ b/apps/api/src/provider/http-schemas/jwt-token.schema.ts @@ -0,0 +1,56 @@ +import type { JwtTokenPayload } from "@akashnetwork/chain-sdk/web"; +import { z } from "zod"; + +const AccessScopeSchema = z.enum(["send-manifest", "get-manifest", "logs", "shell", "events", "status", "restart", "hostname-migrate", "ip-migrate"]); + +const BaseLeasePermissionSchema = z.object({ + provider: z.string() +}); + +const FullAccessPermissionSchema = BaseLeasePermissionSchema.extend({ + access: z.literal("full") +}); + +const ScopedAccessPermissionSchema = BaseLeasePermissionSchema.extend({ + access: z.literal("scoped"), + scope: z.array(AccessScopeSchema) +}); + +const GranularAccessPermissionSchema = BaseLeasePermissionSchema.extend({ + access: z.literal("granular"), + deployments: z.array( + z.object({ + dseq: z.number(), + scope: z.array(AccessScopeSchema), + gseq: z.number().optional(), + oseq: z.number().optional(), + services: z.array(z.string()).optional() + }) + ) +}); + +const LeasePermissionSchema = z.discriminatedUnion("access", [FullAccessPermissionSchema, ScopedAccessPermissionSchema, GranularAccessPermissionSchema]); + +const FullAccessSchema = z.object({ + access: z.literal("full"), + scope: z.array(AccessScopeSchema).optional() +}); + +const GranularAccessSchema = z.object({ + access: z.literal("granular"), + permissions: z.array(LeasePermissionSchema) +}); + +const LeasesSchema = z.discriminatedUnion("access", [FullAccessSchema, GranularAccessSchema]) satisfies z.ZodType; + +export const CreateJwtTokenRequestSchema = z.object({ + ttl: z.number().int().positive(), + leases: LeasesSchema +}); + +export type CreateJwtTokenRequest = z.infer; + +export const CreateJwtTokenResponseSchema = z.object({ + token: z.string() +}); +export type CreateJwtTokenResponse = z.infer; diff --git a/apps/api/src/provider/providers/jwt.provider.ts b/apps/api/src/provider/providers/jwt.provider.ts index c4d197463..8ea454d2a 100644 --- a/apps/api/src/provider/providers/jwt.provider.ts +++ b/apps/api/src/provider/providers/jwt.provider.ts @@ -1,7 +1,12 @@ -import * as jwt from "@akashnetwork/jwt"; +import { createSignArbitraryAkashWallet, JwtTokenManager } from "@akashnetwork/chain-sdk"; import type { InjectionToken } from "tsyringe"; import { container } from "tsyringe"; +const jwt = { + createSignArbitraryAkashWallet, + JwtTokenManager +}; + export type JWTModule = typeof jwt; export const JWT_MODULE: InjectionToken = "JWT_MODULE"; diff --git a/apps/api/src/provider/routes/index.ts b/apps/api/src/provider/routes/index.ts index 0c58930b3..7f384f191 100644 --- a/apps/api/src/provider/routes/index.ts +++ b/apps/api/src/provider/routes/index.ts @@ -7,3 +7,4 @@ export * from "@src/provider/routes/provider-earnings/provider-earnings.router"; export * from "@src/provider/routes/provider-versions/provider-versions.router"; export * from "@src/provider/routes/provider-graph-data/provider-graph-data.router"; export * from "@src/provider/routes/provider-deployments/provider-deployments.router"; +export * from "@src/provider/routes/jwt-token/jwt-token.router"; diff --git a/apps/api/src/provider/routes/jwt-token/jwt-token.router.ts b/apps/api/src/provider/routes/jwt-token/jwt-token.router.ts new file mode 100644 index 000000000..810de6a46 --- /dev/null +++ b/apps/api/src/provider/routes/jwt-token/jwt-token.router.ts @@ -0,0 +1,46 @@ +import { createRoute } from "@hono/zod-openapi"; +import { container } from "tsyringe"; +import { z } from "zod"; + +import { OpenApiHonoHandler } from "@src/core/services/open-api-hono-handler/open-api-hono-handler"; +import { JwtTokenController } from "@src/provider/controllers/jwt-token/jwt-token.controller"; +import { CreateJwtTokenRequestSchema, CreateJwtTokenResponseSchema } from "@src/provider/http-schemas/jwt-token.schema"; + +export const providerJwtTokenRouter = new OpenApiHonoHandler(); + +providerJwtTokenRouter.openapi( + createRoute({ + method: "post", + path: "/v1/create-jwt-token", + summary: "Create new JWT token for managed wallet", + tags: ["JWT Token"], + request: { + body: { + content: { + "application/json": { + schema: z.object({ + data: CreateJwtTokenRequestSchema + }) + } + } + } + }, + responses: { + 201: { + description: "JWT token created successfully", + content: { + "application/json": { + schema: z.object({ + data: CreateJwtTokenResponseSchema + }) + } + } + } + } + }), + async function routeCreateJwtToken(c) { + const body = c.req.valid("json"); + const result = await container.resolve(JwtTokenController).createJwtToken(body.data); + return c.json({ data: result }, 201); + } +); diff --git a/apps/api/src/provider/services/provider-jwt-token/provider-jwt-token.service.spec.ts b/apps/api/src/provider/services/provider-jwt-token/provider-jwt-token.service.spec.ts index 8445bf771..194d587c0 100644 --- a/apps/api/src/provider/services/provider-jwt-token/provider-jwt-token.service.spec.ts +++ b/apps/api/src/provider/services/provider-jwt-token/provider-jwt-token.service.spec.ts @@ -1,4 +1,4 @@ -import type { JwtToken, JwtTokenPayload } from "@akashnetwork/jwt"; +import type { JwtTokenManager, JwtTokenPayload } from "@akashnetwork/chain-sdk"; import type { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; import { faker } from "@faker-js/faker"; import { mock } from "jest-mock-extended"; @@ -21,8 +21,8 @@ describe(ProviderJwtTokenService.name, () => { expect(result).toEqual(jwtTokenValue); expect(walletFactory).toHaveBeenCalledWith(masterWalletMnemonic, walletId); - expect(jwtModule.JwtToken).toHaveBeenCalledWith(akashWallet); - expect(jwtToken.createToken).toHaveBeenCalledWith({ + expect(jwtModule.JwtTokenManager).toHaveBeenCalledWith(akashWallet); + expect(jwtToken.generateToken).toHaveBeenCalledWith({ version: "v1", exp: expect.any(Number), nbf: expect.any(Number), @@ -41,7 +41,7 @@ describe(ProviderJwtTokenService.name, () => { expect(walletFactory).toHaveBeenCalledTimes(1); expect(jwtModule.createSignArbitraryAkashWallet).toHaveBeenCalledTimes(1); - expect(jwtModule.JwtToken).toHaveBeenCalledTimes(1); + expect(jwtModule.JwtTokenManager).toHaveBeenCalledTimes(1); }); describe("getGranularLeases", () => { @@ -83,12 +83,12 @@ describe(ProviderJwtTokenService.name, () => { signArbitrary: jest.fn().mockResolvedValue({ signature: "test-signature" }) }; - const jwtToken = mock(); - jwtToken.createToken.mockImplementation(() => Promise.resolve(jwtTokenValue)); + const jwtToken = mock(); + jwtToken.generateToken.mockImplementation(() => Promise.resolve(jwtTokenValue)); const jwtModule = mock({ createSignArbitraryAkashWallet: jest.fn(async () => akashWallet), - JwtToken: jest.fn(() => jwtToken) + JwtTokenManager: jest.fn(() => jwtToken) }); const billingConfigService = mockConfigService({ diff --git a/apps/api/src/provider/services/provider-jwt-token/provider-jwt-token.service.ts b/apps/api/src/provider/services/provider-jwt-token/provider-jwt-token.service.ts index 9935321e8..79ff749ff 100644 --- a/apps/api/src/provider/services/provider-jwt-token/provider-jwt-token.service.ts +++ b/apps/api/src/provider/services/provider-jwt-token/provider-jwt-token.service.ts @@ -1,4 +1,4 @@ -import { AccessScope, JwtToken, JwtTokenPayload } from "@akashnetwork/jwt"; +import { JwtTokenManager, JwtTokenPayload } from "@akashnetwork/chain-sdk"; import { minutesToSeconds } from "date-fns"; import { inject, singleton } from "tsyringe"; import * as uuid from "uuid"; @@ -12,7 +12,7 @@ import { JWT_MODULE, JWTModule } from "@src/provider/providers/jwt.provider"; const JWT_TOKEN_TTL_IN_SECONDS = 30; type JwtTokenWithAddress = { - jwtToken: JwtToken; + jwtTokenManager: JwtTokenManager; address: string; }; @@ -22,6 +22,8 @@ type GenerateJwtTokenParams = { ttl?: number; }; +type AccessScope = Extract["permissions"][number], { access: "scoped" }>["scope"][number]; + @singleton() export class ProviderJwtTokenService { constructor( @@ -31,10 +33,10 @@ export class ProviderJwtTokenService { ) {} async generateJwtToken({ walletId, leases, ttl = JWT_TOKEN_TTL_IN_SECONDS }: GenerateJwtTokenParams) { - const { jwtToken, address } = await this.getJwtToken(walletId); + const { jwtTokenManager, address } = await this.getJwtToken(walletId); const now = Math.floor(Date.now() / 1000); - return await jwtToken.createToken({ + return await jwtTokenManager.generateToken({ version: "v1", exp: now + ttl, nbf: now, @@ -48,10 +50,10 @@ export class ProviderJwtTokenService { @Memoize({ ttlInSeconds: minutesToSeconds(5) }) private async getJwtToken(walletId: number): Promise { const wallet = this.walletFactory(this.billingConfigService.get("MASTER_WALLET_MNEMONIC"), walletId); - const akashWallet = await this.jwtModule.createSignArbitraryAkashWallet(await wallet.getInstance(), walletId); - const jwtToken = new this.jwtModule.JwtToken(akashWallet); + const akashWallet = await this.jwtModule.createSignArbitraryAkashWallet((await wallet.getInstance()) as any, walletId); + const jwtTokenManager = new this.jwtModule.JwtTokenManager(akashWallet); - return { jwtToken, address: akashWallet.address }; + return { jwtTokenManager, address: akashWallet.address }; } getGranularLeases({ provider, scope }: { provider: string; scope: AccessScope[] }): JwtTokenPayload["leases"] { diff --git a/apps/api/src/provider/services/provider/provider.service.spec.ts b/apps/api/src/provider/services/provider/provider.service.spec.ts index 2142cc1b3..870208db8 100644 --- a/apps/api/src/provider/services/provider/provider.service.spec.ts +++ b/apps/api/src/provider/services/provider/provider.service.spec.ts @@ -1,5 +1,5 @@ +import type { JwtTokenPayload } from "@akashnetwork/chain-sdk"; import type { Provider } from "@akashnetwork/database/dbSchemas/akash"; -import type { JwtTokenPayload } from "@akashnetwork/jwt"; import { netConfig } from "@akashnetwork/net"; import { faker } from "@faker-js/faker"; import { AxiosError } from "axios"; diff --git a/apps/api/src/rest-app.ts b/apps/api/src/rest-app.ts index 0053be840..9b6494eb5 100644 --- a/apps/api/src/rest-app.ts +++ b/apps/api/src/rest-app.ts @@ -65,6 +65,7 @@ import { providerDeploymentsRouter, providerEarningsRouter, providerGraphDataRouter, + providerJwtTokenRouter, providerRegionsRouter, providersRouter, providerVersionsRouter @@ -140,6 +141,7 @@ const openApiHonoHandlers: OpenApiHonoHandler[] = [ providerVersionsRouter, providerGraphDataRouter, providerDeploymentsRouter, + providerJwtTokenRouter, graphDataRouter, dashboardDataRouter, networkCapacityRouter, diff --git a/apps/api/test/functional/__snapshots__/docs.spec.ts.snap b/apps/api/test/functional/__snapshots__/docs.spec.ts.snap index fd7fe4e62..d6a75108f 100644 --- a/apps/api/test/functional/__snapshots__/docs.spec.ts.snap +++ b/apps/api/test/functional/__snapshots__/docs.spec.ts.snap @@ -3707,6 +3707,244 @@ exports[`API Docs GET /v1/doc returns docs with all routes expected 1`] = ` ], }, }, + "/v1/create-jwt-token": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "properties": { + "leases": { + "oneOf": [ + { + "properties": { + "access": { + "enum": [ + "full", + ], + "type": "string", + }, + "scope": { + "items": { + "enum": [ + "send-manifest", + "get-manifest", + "logs", + "shell", + "events", + "status", + "restart", + "hostname-migrate", + "ip-migrate", + ], + "type": "string", + }, + "type": "array", + }, + }, + "required": [ + "access", + ], + "type": "object", + }, + { + "properties": { + "access": { + "enum": [ + "granular", + ], + "type": "string", + }, + "permissions": { + "items": { + "oneOf": [ + { + "properties": { + "access": { + "enum": [ + "full", + ], + "type": "string", + }, + "provider": { + "type": "string", + }, + }, + "required": [ + "provider", + "access", + ], + "type": "object", + }, + { + "properties": { + "access": { + "enum": [ + "scoped", + ], + "type": "string", + }, + "provider": { + "type": "string", + }, + "scope": { + "items": { + "enum": [ + "send-manifest", + "get-manifest", + "logs", + "shell", + "events", + "status", + "restart", + "hostname-migrate", + "ip-migrate", + ], + "type": "string", + }, + "type": "array", + }, + }, + "required": [ + "provider", + "access", + "scope", + ], + "type": "object", + }, + { + "properties": { + "access": { + "enum": [ + "granular", + ], + "type": "string", + }, + "deployments": { + "items": { + "properties": { + "dseq": { + "type": "number", + }, + "gseq": { + "type": "number", + }, + "oseq": { + "type": "number", + }, + "scope": { + "items": { + "enum": [ + "send-manifest", + "get-manifest", + "logs", + "shell", + "events", + "status", + "restart", + "hostname-migrate", + "ip-migrate", + ], + "type": "string", + }, + "type": "array", + }, + "services": { + "items": { + "type": "string", + }, + "type": "array", + }, + }, + "required": [ + "dseq", + "scope", + ], + "type": "object", + }, + "type": "array", + }, + "provider": { + "type": "string", + }, + }, + "required": [ + "provider", + "access", + "deployments", + ], + "type": "object", + }, + ], + }, + "type": "array", + }, + }, + "required": [ + "access", + "permissions", + ], + "type": "object", + }, + ], + }, + "ttl": { + "exclusiveMinimum": true, + "minimum": 0, + "type": "integer", + }, + }, + "required": [ + "ttl", + "leases", + ], + "type": "object", + }, + }, + "required": [ + "data", + ], + "type": "object", + }, + }, + }, + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "properties": { + "token": { + "type": "string", + }, + }, + "required": [ + "token", + ], + "type": "object", + }, + }, + "required": [ + "data", + ], + "type": "object", + }, + }, + }, + "description": "JWT token created successfully", + }, + }, + "summary": "Create new JWT token for managed wallet", + "tags": [ + "JWT Token", + ], + }, + }, "/v1/dashboard-data": { "get": { "responses": { diff --git a/apps/provider-proxy/package.json b/apps/provider-proxy/package.json index 56510604b..825e6e7b0 100644 --- a/apps/provider-proxy/package.json +++ b/apps/provider-proxy/package.json @@ -22,7 +22,7 @@ "test:unit": "jest --selectProjects unit" }, "dependencies": { - "@akashnetwork/jwt": "*", + "@akashnetwork/chain-sdk": "^1.0.0-alpha.3", "@akashnetwork/logging": "*", "@akashnetwork/net": "*", "@cosmjs/encoding": "^0.32.4", @@ -41,7 +41,7 @@ "@akashnetwork/dev-config": "*", "@akashnetwork/docker": "*", "@akashnetwork/releaser": "*", - "@cosmjs/proto-signing": "^0.32.4", + "@cosmjs/proto-signing": "^0.33.1", "@types/cors": "^2.8.13", "@types/express": "^4.17.16", "@types/node": "^22.13.11", diff --git a/apps/provider-proxy/src/utils/schema.ts b/apps/provider-proxy/src/utils/schema.ts index a71f25908..f7e72daa8 100644 --- a/apps/provider-proxy/src/utils/schema.ts +++ b/apps/provider-proxy/src/utils/schema.ts @@ -1,4 +1,4 @@ -import { JwtToken } from "@akashnetwork/jwt"; +import { JwtTokenManager } from "@akashnetwork/chain-sdk"; import type { SupportedChainNetworks } from "@akashnetwork/net"; import { netConfig } from "@akashnetwork/net"; import { z } from "@hono/zod-openapi"; @@ -33,7 +33,7 @@ export const providerRequestSchema = z.object({ // we need just validation and decoding that's why signer is not provided // eslint-disable-next-line @typescript-eslint/no-explicit-any -const jwtTokenManager = new JwtToken({} as any); +const jwtTokenManager = new JwtTokenManager({} as any); export type ProviderRequestSchema = Omit, "chainNetwork" | "certPem" | "keyPem">; diff --git a/apps/provider-proxy/test/functional/provider-proxy-http.spec.ts b/apps/provider-proxy/test/functional/provider-proxy-http.spec.ts index b328212e3..21c5b0e60 100644 --- a/apps/provider-proxy/test/functional/provider-proxy-http.spec.ts +++ b/apps/provider-proxy/test/functional/provider-proxy-http.spec.ts @@ -1,4 +1,4 @@ -import { createSignArbitraryAkashWallet, JwtToken } from "@akashnetwork/jwt"; +import { createSignArbitraryAkashWallet, JwtTokenManager } from "@akashnetwork/chain-sdk"; import type { SupportedChainNetworks } from "@akashnetwork/net"; import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; import { setTimeout as wait } from "timers/promises"; @@ -682,8 +682,8 @@ describe("Provider HTTP proxy", () => { const testMnemonic = "body letter input area umbrella develop shuffle gentle regular gold twice truly giant dawn nerve ocean wine wonder toe melt grid leader blush few"; const wallet = await DirectSecp256k1HdWallet.fromMnemonic(testMnemonic, { prefix: "akash" }); - const tokenManager = new JwtToken(await createSignArbitraryAkashWallet(wallet)); - const token = await tokenManager.createToken({ + const tokenManager = new JwtTokenManager(await createSignArbitraryAkashWallet(wallet)); + const token = await tokenManager.generateToken({ iss: (await wallet.getAccounts())[0].address, exp: Math.floor(Date.now() / 1000) + 3600, iat: Math.floor(Date.now() / 1000), diff --git a/apps/provider-proxy/test/functional/provider-proxy-ws.spec.ts b/apps/provider-proxy/test/functional/provider-proxy-ws.spec.ts index e4cea9ce5..eabd348cc 100644 --- a/apps/provider-proxy/test/functional/provider-proxy-ws.spec.ts +++ b/apps/provider-proxy/test/functional/provider-proxy-ws.spec.ts @@ -1,4 +1,4 @@ -import { createSignArbitraryAkashWallet, JwtToken } from "@akashnetwork/jwt"; +import { createSignArbitraryAkashWallet, JwtTokenManager } from "@akashnetwork/chain-sdk"; import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; import { setTimeout } from "timers/promises"; import type { TLSSocket } from "tls"; @@ -313,8 +313,8 @@ describe("Provider proxy ws", () => { const testMnemonic = "body letter input area umbrella develop shuffle gentle regular gold twice truly giant dawn nerve ocean wine wonder toe melt grid leader blush few"; const wallet = await DirectSecp256k1HdWallet.fromMnemonic(testMnemonic, { prefix: "akash" }); - const tokenManager = new JwtToken(await createSignArbitraryAkashWallet(wallet)); - const token = await tokenManager.createToken({ + const tokenManager = new JwtTokenManager(await createSignArbitraryAkashWallet(wallet)); + const token = await tokenManager.generateToken({ iss: (await wallet.getAccounts())[0].address, exp: Math.floor(Date.now() / 1000) + 3600, iat: Math.floor(Date.now() / 1000), diff --git a/apps/provider-proxy/tsconfig.build.json b/apps/provider-proxy/tsconfig.build.json index cef86b58f..88fe63538 100644 --- a/apps/provider-proxy/tsconfig.build.json +++ b/apps/provider-proxy/tsconfig.build.json @@ -3,7 +3,8 @@ "strict": true, "isolatedModules": true, "target": "es2022", - "lib": ["ESNext", "ES2024"] + "lib": ["ESNext", "ES2024"], + "module": "NodeNext", }, "extends": "@akashnetwork/dev-config/tsconfig.base-node.json" } diff --git a/package-lock.json b/package-lock.json index 0d20198f4..5f21cbea0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "dependencies": { "@akashnetwork/akash-api": "^1.3.0", "@akashnetwork/akashjs": "^0.11.1", + "@akashnetwork/chain-sdk": "^1.0.0-alpha.3", "@akashnetwork/database": "*", "@akashnetwork/env-loader": "*", "@akashnetwork/http-sdk": "*", @@ -894,6 +895,7 @@ "dependencies": { "@akashnetwork/akash-api": "^1.3.0", "@akashnetwork/akashjs": "^0.11.1", + "@akashnetwork/chain-sdk": "^1.0.0-alpha.3", "@akashnetwork/env-loader": "*", "@akashnetwork/http-sdk": "*", "@akashnetwork/jwt": "*", @@ -4566,7 +4568,7 @@ "version": "1.20.0", "license": "Apache-2.0", "dependencies": { - "@akashnetwork/jwt": "*", + "@akashnetwork/chain-sdk": "^1.0.0-alpha.3", "@akashnetwork/logging": "*", "@akashnetwork/net": "*", "@cosmjs/encoding": "^0.32.4", @@ -4585,7 +4587,7 @@ "@akashnetwork/dev-config": "*", "@akashnetwork/docker": "*", "@akashnetwork/releaser": "*", - "@cosmjs/proto-signing": "^0.32.4", + "@cosmjs/proto-signing": "^0.33.1", "@types/cors": "^2.8.13", "@types/express": "^4.17.16", "@types/node": "^22.13.11", @@ -4607,6 +4609,60 @@ "typescript": "~5.8.2" } }, + "apps/provider-proxy/node_modules/@cosmjs/amino": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@cosmjs/amino/-/amino-0.33.1.tgz", + "integrity": "sha512-WfWiBf2EbIWpwKG9AOcsIIkR717SY+JdlXM/SL/bI66BdrhniAF+/ZNis9Vo9HF6lP2UU5XrSmFA4snAvEgdrg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@cosmjs/crypto": "^0.33.1", + "@cosmjs/encoding": "^0.33.1", + "@cosmjs/math": "^0.33.1", + "@cosmjs/utils": "^0.33.1" + } + }, + "apps/provider-proxy/node_modules/@cosmjs/amino/node_modules/@cosmjs/encoding": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@cosmjs/encoding/-/encoding-0.33.1.tgz", + "integrity": "sha512-nuNxf29fUcQE14+1p//VVQDwd1iau5lhaW/7uMz7V2AH3GJbFJoJVaKvVyZvdFk+Cnu+s3wCqgq4gJkhRCJfKw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "bech32": "^1.1.4", + "readonly-date": "^1.0.0" + } + }, + "apps/provider-proxy/node_modules/@cosmjs/crypto": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@cosmjs/crypto/-/crypto-0.33.1.tgz", + "integrity": "sha512-U4kGIj/SNBzlb2FGgA0sMR0MapVgJUg8N+oIAiN5+vl4GZ3aefmoL1RDyTrFS/7HrB+M+MtHsxC0tvEu4ic/zA==", + "deprecated": "This uses elliptic for cryptographic operations, which contains several security-relevant bugs. To what degree this affects your application is something you need to carefully investigate. See https://github.com/cosmos/cosmjs/issues/1708 for further pointers. Starting with version 0.34.0 the cryptographic library has been replaced. However, private keys might still be at risk.", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@cosmjs/encoding": "^0.33.1", + "@cosmjs/math": "^0.33.1", + "@cosmjs/utils": "^0.33.1", + "@noble/hashes": "^1", + "bn.js": "^5.2.0", + "elliptic": "^6.6.1", + "libsodium-wrappers-sumo": "^0.7.11" + } + }, + "apps/provider-proxy/node_modules/@cosmjs/crypto/node_modules/@cosmjs/encoding": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@cosmjs/encoding/-/encoding-0.33.1.tgz", + "integrity": "sha512-nuNxf29fUcQE14+1p//VVQDwd1iau5lhaW/7uMz7V2AH3GJbFJoJVaKvVyZvdFk+Cnu+s3wCqgq4gJkhRCJfKw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "bech32": "^1.1.4", + "readonly-date": "^1.0.0" + } + }, "apps/provider-proxy/node_modules/@cosmjs/encoding": { "version": "0.32.4", "resolved": "https://registry.npmjs.org/@cosmjs/encoding/-/encoding-0.32.4.tgz", @@ -4618,6 +4674,50 @@ "readonly-date": "^1.0.0" } }, + "apps/provider-proxy/node_modules/@cosmjs/math": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@cosmjs/math/-/math-0.33.1.tgz", + "integrity": "sha512-ytGkWdKFCPiiBU5eqjHNd59djPpIsOjbr2CkNjlnI1Zmdj+HDkSoD9MUGpz9/RJvRir5IvsXqdE05x8EtoQkJA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.2.0" + } + }, + "apps/provider-proxy/node_modules/@cosmjs/proto-signing": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@cosmjs/proto-signing/-/proto-signing-0.33.1.tgz", + "integrity": "sha512-Sv4W+MxX+0LVnd+2rU4Fw1HRsmMwSVSYULj7pRkij3wnPwUlTVoJjmKFgKz13ooIlfzPrz/dnNjGp/xnmXChFQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@cosmjs/amino": "^0.33.1", + "@cosmjs/crypto": "^0.33.1", + "@cosmjs/encoding": "^0.33.1", + "@cosmjs/math": "^0.33.1", + "@cosmjs/utils": "^0.33.1", + "cosmjs-types": "^0.9.0" + } + }, + "apps/provider-proxy/node_modules/@cosmjs/proto-signing/node_modules/@cosmjs/encoding": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@cosmjs/encoding/-/encoding-0.33.1.tgz", + "integrity": "sha512-nuNxf29fUcQE14+1p//VVQDwd1iau5lhaW/7uMz7V2AH3GJbFJoJVaKvVyZvdFk+Cnu+s3wCqgq4gJkhRCJfKw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "bech32": "^1.1.4", + "readonly-date": "^1.0.0" + } + }, + "apps/provider-proxy/node_modules/@cosmjs/utils": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@cosmjs/utils/-/utils-0.33.1.tgz", + "integrity": "sha512-UnLHDY6KMmC+UXf3Ufyh+onE19xzEXjT4VZ504Acmk4PXxqyvG4cCPprlKUFnGUX7f0z8Or9MAOHXBx41uHBcg==", + "dev": true, + "license": "Apache-2.0" + }, "apps/provider-proxy/node_modules/@esbuild/aix-ppc64": { "version": "0.24.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", @@ -5843,6 +5943,194 @@ "url": "https://dotenvx.com" } }, + "node_modules/@akashnetwork/chain-sdk": { + "version": "1.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@akashnetwork/chain-sdk/-/chain-sdk-1.0.0-alpha.3.tgz", + "integrity": "sha512-fCc39LFpYVr9D2hoVg83+FQ9hCZGcI1rBW9c/kyqUSL/uIgpUPzljyoQnmupoVaCTlJY1VurP5aHNk4VHuRXAA==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^2.2.3", + "@connectrpc/connect": "^2.0.1", + "@connectrpc/connect-node": "^2.0.1", + "@cosmjs/amino": "^0.32.4", + "@cosmjs/math": "^0.33.1", + "@cosmjs/proto-signing": "^0.33.1", + "@cosmjs/stargate": "^0.33.1", + "js-yaml": "^4.1.0", + "json-stable-stringify": "^1.3.0", + "jsrsasign": "^11.1.0", + "long": "^5.3.2" + }, + "engines": { + "node": "22.14.0" + } + }, + "node_modules/@akashnetwork/chain-sdk/node_modules/@cosmjs/crypto": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@cosmjs/crypto/-/crypto-0.33.1.tgz", + "integrity": "sha512-U4kGIj/SNBzlb2FGgA0sMR0MapVgJUg8N+oIAiN5+vl4GZ3aefmoL1RDyTrFS/7HrB+M+MtHsxC0tvEu4ic/zA==", + "deprecated": "This uses elliptic for cryptographic operations, which contains several security-relevant bugs. To what degree this affects your application is something you need to carefully investigate. See https://github.com/cosmos/cosmjs/issues/1708 for further pointers. Starting with version 0.34.0 the cryptographic library has been replaced. However, private keys might still be at risk.", + "license": "Apache-2.0", + "dependencies": { + "@cosmjs/encoding": "^0.33.1", + "@cosmjs/math": "^0.33.1", + "@cosmjs/utils": "^0.33.1", + "@noble/hashes": "^1", + "bn.js": "^5.2.0", + "elliptic": "^6.6.1", + "libsodium-wrappers-sumo": "^0.7.11" + } + }, + "node_modules/@akashnetwork/chain-sdk/node_modules/@cosmjs/encoding": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@cosmjs/encoding/-/encoding-0.33.1.tgz", + "integrity": "sha512-nuNxf29fUcQE14+1p//VVQDwd1iau5lhaW/7uMz7V2AH3GJbFJoJVaKvVyZvdFk+Cnu+s3wCqgq4gJkhRCJfKw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "bech32": "^1.1.4", + "readonly-date": "^1.0.0" + } + }, + "node_modules/@akashnetwork/chain-sdk/node_modules/@cosmjs/json-rpc": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@cosmjs/json-rpc/-/json-rpc-0.33.1.tgz", + "integrity": "sha512-T6VtWzecpmuTuMRGZWuBYHsMF/aznWCYUt/cGMWNSz7DBPipVd0w774PKpxXzpEbyt5sr61NiuLXc+Az15S/Cw==", + "license": "Apache-2.0", + "dependencies": { + "@cosmjs/stream": "^0.33.1", + "xstream": "^11.14.0" + } + }, + "node_modules/@akashnetwork/chain-sdk/node_modules/@cosmjs/math": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@cosmjs/math/-/math-0.33.1.tgz", + "integrity": "sha512-ytGkWdKFCPiiBU5eqjHNd59djPpIsOjbr2CkNjlnI1Zmdj+HDkSoD9MUGpz9/RJvRir5IvsXqdE05x8EtoQkJA==", + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.2.0" + } + }, + "node_modules/@akashnetwork/chain-sdk/node_modules/@cosmjs/proto-signing": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@cosmjs/proto-signing/-/proto-signing-0.33.1.tgz", + "integrity": "sha512-Sv4W+MxX+0LVnd+2rU4Fw1HRsmMwSVSYULj7pRkij3wnPwUlTVoJjmKFgKz13ooIlfzPrz/dnNjGp/xnmXChFQ==", + "license": "Apache-2.0", + "dependencies": { + "@cosmjs/amino": "^0.33.1", + "@cosmjs/crypto": "^0.33.1", + "@cosmjs/encoding": "^0.33.1", + "@cosmjs/math": "^0.33.1", + "@cosmjs/utils": "^0.33.1", + "cosmjs-types": "^0.9.0" + } + }, + "node_modules/@akashnetwork/chain-sdk/node_modules/@cosmjs/proto-signing/node_modules/@cosmjs/amino": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@cosmjs/amino/-/amino-0.33.1.tgz", + "integrity": "sha512-WfWiBf2EbIWpwKG9AOcsIIkR717SY+JdlXM/SL/bI66BdrhniAF+/ZNis9Vo9HF6lP2UU5XrSmFA4snAvEgdrg==", + "license": "Apache-2.0", + "dependencies": { + "@cosmjs/crypto": "^0.33.1", + "@cosmjs/encoding": "^0.33.1", + "@cosmjs/math": "^0.33.1", + "@cosmjs/utils": "^0.33.1" + } + }, + "node_modules/@akashnetwork/chain-sdk/node_modules/@cosmjs/socket": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@cosmjs/socket/-/socket-0.33.1.tgz", + "integrity": "sha512-KzAeorten6Vn20sMiM6NNWfgc7jbyVo4Zmxev1FXa5EaoLCZy48cmT3hJxUJQvJP/lAy8wPGEjZ/u4rmF11x9A==", + "license": "Apache-2.0", + "dependencies": { + "@cosmjs/stream": "^0.33.1", + "isomorphic-ws": "^4.0.1", + "ws": "^7", + "xstream": "^11.14.0" + } + }, + "node_modules/@akashnetwork/chain-sdk/node_modules/@cosmjs/stargate": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@cosmjs/stargate/-/stargate-0.33.1.tgz", + "integrity": "sha512-CnJ1zpSiaZgkvhk+9aTp5IPmgWn2uo+cNEBN8VuD9sD6BA0V4DMjqe251cNFLiMhkGtiE5I/WXFERbLPww3k8g==", + "license": "Apache-2.0", + "dependencies": { + "@cosmjs/amino": "^0.33.1", + "@cosmjs/encoding": "^0.33.1", + "@cosmjs/math": "^0.33.1", + "@cosmjs/proto-signing": "^0.33.1", + "@cosmjs/stream": "^0.33.1", + "@cosmjs/tendermint-rpc": "^0.33.1", + "@cosmjs/utils": "^0.33.1", + "cosmjs-types": "^0.9.0" + } + }, + "node_modules/@akashnetwork/chain-sdk/node_modules/@cosmjs/stargate/node_modules/@cosmjs/amino": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@cosmjs/amino/-/amino-0.33.1.tgz", + "integrity": "sha512-WfWiBf2EbIWpwKG9AOcsIIkR717SY+JdlXM/SL/bI66BdrhniAF+/ZNis9Vo9HF6lP2UU5XrSmFA4snAvEgdrg==", + "license": "Apache-2.0", + "dependencies": { + "@cosmjs/crypto": "^0.33.1", + "@cosmjs/encoding": "^0.33.1", + "@cosmjs/math": "^0.33.1", + "@cosmjs/utils": "^0.33.1" + } + }, + "node_modules/@akashnetwork/chain-sdk/node_modules/@cosmjs/stream": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@cosmjs/stream/-/stream-0.33.1.tgz", + "integrity": "sha512-bMUvEENjeQPSTx+YRzVsWT1uFIdHRcf4brsc14SOoRQ/j5rOJM/aHfsf/BmdSAnYbdOQ3CMKj/8nGAQ7xUdn7w==", + "license": "Apache-2.0", + "dependencies": { + "xstream": "^11.14.0" + } + }, + "node_modules/@akashnetwork/chain-sdk/node_modules/@cosmjs/tendermint-rpc": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@cosmjs/tendermint-rpc/-/tendermint-rpc-0.33.1.tgz", + "integrity": "sha512-22klDFq2MWnf//C8+rZ5/dYatr6jeGT+BmVbutXYfAK9fmODbtFcumyvB6uWaEORWfNukl8YK1OLuaWezoQvxA==", + "license": "Apache-2.0", + "dependencies": { + "@cosmjs/crypto": "^0.33.1", + "@cosmjs/encoding": "^0.33.1", + "@cosmjs/json-rpc": "^0.33.1", + "@cosmjs/math": "^0.33.1", + "@cosmjs/socket": "^0.33.1", + "@cosmjs/stream": "^0.33.1", + "@cosmjs/utils": "^0.33.1", + "axios": "^1.6.0", + "readonly-date": "^1.0.0", + "xstream": "^11.14.0" + } + }, + "node_modules/@akashnetwork/chain-sdk/node_modules/@cosmjs/utils": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@cosmjs/utils/-/utils-0.33.1.tgz", + "integrity": "sha512-UnLHDY6KMmC+UXf3Ufyh+onE19xzEXjT4VZ504Acmk4PXxqyvG4cCPprlKUFnGUX7f0z8Or9MAOHXBx41uHBcg==", + "license": "Apache-2.0" + }, + "node_modules/@akashnetwork/chain-sdk/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@akashnetwork/console-api": { "resolved": "apps/api", "link": true @@ -8159,6 +8447,12 @@ "dev": true, "license": "MIT OR Apache-2.0" }, + "node_modules/@bufbuild/protobuf": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.9.0.tgz", + "integrity": "sha512-rnJenoStJ8nvmt9Gzye8nkYd6V22xUAnu4086ER7h1zJ508vStko4pMvDeQ446ilDTFpV5wnoc5YS7XvMwwMqA==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, "node_modules/@casl/ability": { "version": "6.7.2", "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.7.2.tgz", @@ -8699,6 +8993,28 @@ "protobufjs": "^6.8.8" } }, + "node_modules/@connectrpc/connect": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-2.1.0.tgz", + "integrity": "sha512-xhiwnYlJNHzmFsRw+iSPIwXR/xweTvTw8x5HiwWp10sbVtd4OpOXbRgE7V58xs1EC17fzusF1f5uOAy24OkBuA==", + "license": "Apache-2.0", + "peerDependencies": { + "@bufbuild/protobuf": "^2.7.0" + } + }, + "node_modules/@connectrpc/connect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect-node/-/connect-node-2.1.0.tgz", + "integrity": "sha512-6akCXZSX5uWHLR654ne9Tnq7AnPUkLS65NvgsI5885xBkcuVy2APBd8sA4sLqaplUt84cVEr6LhjEFNx6W1KtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@bufbuild/protobuf": "^2.7.0", + "@connectrpc/connect": "2.1.0" + } + }, "node_modules/@cosmjs/amino": { "version": "0.32.4", "resolved": "https://registry.npmjs.org/@cosmjs/amino/-/amino-0.32.4.tgz", @@ -29196,14 +29512,15 @@ } }, "node_modules/call-bind": { - "version": "1.0.7", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" }, "engines": { "node": ">= 0.4" @@ -38666,10 +38983,13 @@ "license": "MIT" }, "node_modules/json-stable-stringify": { - "version": "1.1.1", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "isarray": "^2.0.5", "jsonify": "^0.0.1", "object-keys": "^1.1.1" @@ -39845,9 +40165,10 @@ } }, "node_modules/long": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz", - "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==" + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" }, "node_modules/longest-streak": { "version": "3.1.0", diff --git a/packages/jwt/.prettierrc.js b/packages/jwt/.prettierrc.js deleted file mode 100644 index 8e89b39c8..000000000 --- a/packages/jwt/.prettierrc.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("@akashnetwork/dev-config/.prettierrc"); diff --git a/packages/jwt/jest.config.ts b/packages/jwt/jest.config.ts deleted file mode 100644 index 7203ccdaf..000000000 --- a/packages/jwt/jest.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Config } from "jest"; - -export default { - preset: "ts-jest", - testEnvironment: "node", - testMatch: ["**/src/**/*.spec.ts"], - verbose: true -} satisfies Config; diff --git a/packages/jwt/package.json b/packages/jwt/package.json deleted file mode 100644 index e847f89fd..000000000 --- a/packages/jwt/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "@akashnetwork/jwt", - "version": "0.0.1", - "description": "JWT schema for Akash provider authentication", - "repository": { - "type": "git", - "url": "https://github.com/akash-network/console" - }, - "license": "Apache-2.0", - "author": "Akash Network", - "main": "src/index.ts", - "scripts": { - "format": "prettier --write ./*.{ts,json} 'src/**/*.{ts,json}'", - "generate": "node --experimental-strip-types scripts/generate.ts && npm run format", - "lint": "eslint .", - "test": "npm run generate && jest", - "test:cov": "npm run test -- --coverage", - "validate:types": "tsc --noEmit && echo" - }, - "dependencies": { - "@cosmjs/amino": "^0.32.4", - "@cosmjs/encoding": "^0.32.4", - "ajv": "^8.17.1", - "ajv-formats": "^2.1.1", - "base64url": "^3.0.1", - "cosmjs-types": "^0.9.0", - "did-jwt": "^8.0.14", - "did-resolver": "^4.1.0" - }, - "devDependencies": { - "@cosmjs/crypto": "^0.32.4", - "@cosmjs/proto-signing": "^0.32.4", - "@faker-js/faker": "^9.7.0", - "@types/elliptic": "^6.4.18", - "jest": "^29.7.0", - "typescript": "~5.8.2" - } -} diff --git a/packages/jwt/scripts/generate.ts b/packages/jwt/scripts/generate.ts deleted file mode 100644 index 6882c1829..000000000 --- a/packages/jwt/scripts/generate.ts +++ /dev/null @@ -1,70 +0,0 @@ -import * as fsp from "fs/promises"; -import { dirname, join as joinPath, normalize } from "path"; -import { fileURLToPath } from "url"; - -const scriptDir = dirname(fileURLToPath(import.meta.url)); -const PROJECT_DIR = normalize(joinPath(scriptDir, "..", "..", "..")); -const PACKAGE_DIR = normalize(joinPath(scriptDir, "..")); -const OUT_TEST_DIR = joinPath(PACKAGE_DIR, "src", "test", "generated"); -const OUT_DIR = joinPath(PACKAGE_DIR, "src", "generated"); -const BRANCH = process.env.AKASH_API_BRANCH || "refs/heads/main"; -const JWT_SCHEMA_URL = `https://raw.githubusercontent.com/akash-network/akash-api/${BRANCH}/specs/jwt-schema.json`; -const JWT_SIGNING_TEST_CASES_URL = `https://raw.githubusercontent.com/akash-network/akash-api/${BRANCH}/testdata/jwt/cases_es256k.json`; -const JWT_CLAIMS_TEST_CASES_URL = `https://raw.githubusercontent.com/akash-network/akash-api/${BRANCH}/testdata/jwt/cases_jwt.json.tmpl`; -const JWT_MNEMONIC_URL = `https://raw.githubusercontent.com/akash-network/akash-api/${BRANCH}/testdata/jwt/mnemonic`; - -async function main() { - console.log(`Generating JWT schema and test cases`); - - try { - const [schema, signingTestCases, claimsTestCases, mnemonic] = await Promise.all([ - fetchJson(JWT_SCHEMA_URL), - fetchJson(JWT_SIGNING_TEST_CASES_URL), - fetchJson(JWT_CLAIMS_TEST_CASES_URL), - fetchText(JWT_MNEMONIC_URL) - ]); - - await fsp.mkdir(OUT_DIR, { recursive: true }); - - await Promise.all([ - fsp.writeFile(joinPath(OUT_DIR, "jwt-schema-data.ts"), `export const jwtSchemaData = ${JSON.stringify(schema, null, 2)}`), - fsp.writeFile( - joinPath(OUT_TEST_DIR, "jwt-signing-test-cases.ts"), - `// This file contains test cases for JWT signing validation\nexport const jwtSigningTestCases = ${JSON.stringify(signingTestCases, null, 2)};` - ), - fsp.writeFile( - joinPath(OUT_TEST_DIR, "jwt-claims-test-cases.ts"), - `// This file contains test cases for JWT claims validation\nexport const jwtClaimsTestCases = ${JSON.stringify(claimsTestCases, null, 2)};` - ), - fsp.writeFile( - joinPath(OUT_TEST_DIR, "jwt-mnemonic.ts"), - `// This file contains the test mnemonic for JWT signing\nexport const jwtMnemonic = "${mnemonic.trim()}";` - ) - ]); - - console.log(`JWT schema and test cases were written to ${OUT_DIR.replace(PROJECT_DIR, ".")}/`); - } catch (error) { - console.error("Failed to generate JWT schema and test cases:", error); - process.exit(1); - } -} - -function fetchJson(url: string): Promise { - return fetch(url).then(res => { - if (!res.ok) { - throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`); - } - return res.json(); - }); -} - -function fetchText(url: string): Promise { - return fetch(url).then(res => { - if (!res.ok) { - throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`); - } - return res.text(); - }); -} - -main().catch(console.error); diff --git a/packages/jwt/src/base64.ts b/packages/jwt/src/base64.ts deleted file mode 100644 index 6ad5271b1..000000000 --- a/packages/jwt/src/base64.ts +++ /dev/null @@ -1,14 +0,0 @@ -export function base64UrlEncode(str: string): string { - const base64 = Buffer.from(str).toString("base64"); - return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); -} - -/** - * Decode a base64 string - * @param base64String The base64 string to decode - * @returns The decoded object - */ -export function base64Decode(base64String: string): Record { - const decoded = Buffer.from(base64String, "base64").toString("utf8"); - return JSON.parse(decoded); -} diff --git a/packages/jwt/src/generated/jwt-schema-data.ts b/packages/jwt/src/generated/jwt-schema-data.ts deleted file mode 100644 index ea440c238..000000000 --- a/packages/jwt/src/generated/jwt-schema-data.ts +++ /dev/null @@ -1,284 +0,0 @@ -export const jwtSchemaData = { - $schema: "http://json-schema.org/draft-07/schema#", - $id: "https://raw.githubusercontent.com/akash-network/akash-api/refs/heads/main/specs/jwt-schema.json", - title: "Akash JWT Schema", - description: "JSON Schema for JWT used in the Akash Provider API.", - type: "object", - additionalProperties: false, - required: ["iss", "iat", "exp", "nbf", "version", "leases"], - properties: { - iss: { - type: "string", - pattern: "^akash1[a-z0-9]{38}$", - description: "Akash address of the lease(s) owner, e.g., akash1abcd... (44 characters)" - }, - iat: { - type: "integer", - minimum: 0, - description: "Token issuance timestamp as Unix time (seconds since 1970-01-01T00:00:00Z). Should be <= exp and >= nbf." - }, - nbf: { - type: "integer", - minimum: 0, - description: "Not valid before timestamp as Unix time (seconds since 1970-01-01T00:00:00Z). Should be <= iat." - }, - exp: { - type: "integer", - minimum: 0, - description: "Expiration timestamp as Unix time (seconds since 1970-01-01T00:00:00Z). Should be >= iat." - }, - jti: { - type: "string", - minLength: 1, - description: "Unique identifier for the JWT, used to prevent token reuse." - }, - version: { - type: "string", - enum: ["v1"], - description: "Version of the JWT specification (currently fixed at v1)." - }, - leases: { - type: "object", - additionalProperties: false, - required: ["access"], - properties: { - access: { - type: "string", - enum: ["full", "granular"], - description: "Access level for the lease: 'full' for unrestricted access to all actions, 'granular' for provider-specific permissions." - }, - scope: { - type: "array", - minItems: 1, - uniqueItems: true, - items: { - type: "string", - enum: ["send-manifest", "get-manifest", "logs", "shell", "events", "status", "restart", "hostname-migrate", "ip-migrate"] - }, - description: "Global list of permitted actions across all owned leases (no duplicates). Optional when access is 'full'." - }, - permissions: { - type: "array", - description: - "Required if leases.access is 'granular'; defines provider-specific permissions. The provider address must be unique across all permissions entries.", - minItems: 1, - items: { - type: "object", - additionalProperties: false, - required: ["provider", "access"], - properties: { - provider: { - type: "string", - pattern: "^akash1[a-z0-9]{38}$", - description: "Provider address, e.g., akash1xyz... (44 characters)." - }, - access: { - type: "string", - enum: ["full", "scoped", "granular"], - description: - "Provider-level access: 'full' for all actions, 'scoped' for specific actions across all provider leases, 'granular' for deployment-specific actions." - }, - scope: { - type: "array", - minItems: 1, - uniqueItems: true, - items: { - type: "string", - enum: ["send-manifest", "get-manifest", "logs", "shell", "events", "status", "restart", "hostname-migrate", "ip-migrate"] - }, - description: "Provider-level list of permitted actions for 'scoped' access (no duplicates)." - }, - deployments: { - type: "array", - minItems: 1, - items: { - type: "object", - additionalProperties: false, - required: ["dseq", "scope", "services"], - properties: { - dseq: { - type: "integer", - minimum: 1, - description: "Deployment sequence number." - }, - scope: { - type: "array", - minItems: 1, - uniqueItems: true, - items: { - type: "string", - enum: ["send-manifest", "get-manifest", "logs", "shell", "events", "status", "restart", "hostname-migrate", "ip-migrate"] - }, - description: "Deployment-level list of permitted actions (no duplicates)." - }, - gseq: { - type: "integer", - minimum: 0, - description: "Group sequence number (requires dseq)." - }, - oseq: { - type: "integer", - minimum: 0, - description: "Order sequence number (requires dseq and gseq)." - }, - services: { - type: "array", - minItems: 1, - items: { - type: "string", - minLength: 1 - }, - description: "List of service names (requires dseq)." - } - }, - dependencies: { - gseq: ["dseq"], - oseq: ["dseq", "gseq"], - services: ["dseq"] - } - } - } - }, - allOf: [ - { - if: { - properties: { - access: { - const: "scoped" - } - } - }, - then: { - required: ["scope"], - properties: { - scope: { - minItems: 1 - }, - deployments: false - } - } - }, - { - if: { - properties: { - access: { - const: "granular" - } - } - }, - then: { - required: ["deployments"], - properties: { - scope: false - } - } - }, - { - if: { - properties: { - access: { - const: "full" - } - } - }, - then: { - properties: { - scope: false, - deployments: false - } - } - } - ] - } - } - }, - allOf: [ - { - if: { - properties: { - access: { - const: "full" - } - } - }, - then: { - properties: { - permissions: false - } - } - }, - { - if: { - properties: { - access: { - const: "granular" - } - }, - required: ["access"] - }, - then: { - required: ["permissions"], - properties: { - scope: false - } - } - } - ] - } - }, - allOf: [ - { - if: { - properties: { - leases: { - properties: { - access: { - const: "granular" - } - }, - required: ["access"] - } - }, - required: ["leases"] - }, - then: { - properties: { - leases: { - required: ["permissions"], - properties: { - scope: false - } - } - } - } - }, - { - if: { - properties: { - leases: { - properties: { - permissions: { - type: "array", - minItems: 1 - } - }, - required: ["permissions"] - } - }, - required: ["leases"] - }, - then: { - properties: { - leases: { - properties: { - access: { - const: "granular" - } - }, - required: ["access"] - } - } - } - } - ] -}; diff --git a/packages/jwt/src/index.ts b/packages/jwt/src/index.ts deleted file mode 100644 index 3c7d034a3..000000000 --- a/packages/jwt/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { JwtToken, type CreateJWTOptions } from "./jwt-token"; -export type { JwtTokenPayload, AccessScope, LeasePermission } from "./types"; -export { JwtValidator, type JwtValidationResult } from "./jwt-validator"; -export { createSignArbitraryAkashWallet, type SignArbitraryAkashWallet } from "./wallet-utils"; diff --git a/packages/jwt/src/jwt-token.spec.ts b/packages/jwt/src/jwt-token.spec.ts deleted file mode 100644 index 4f6ac5601..000000000 --- a/packages/jwt/src/jwt-token.spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; - -import { jwtClaimsTestCases } from "./test/generated/jwt-claims-test-cases"; -import { jwtMnemonic } from "./test/generated/jwt-mnemonic"; -import { jwtSigningTestCases } from "./test/generated/jwt-signing-test-cases"; -import { replaceTemplateValues } from "./test/test-utils"; -import type { CreateJWTOptions } from "./jwt-token"; -import { JwtToken } from "./jwt-token"; -import { createSignArbitraryAkashWallet, type SignArbitraryAkashWallet } from "./wallet-utils"; - -describe("JWT Claims Validation", () => { - let testWallet: DirectSecp256k1HdWallet; - let jwtToken: JwtToken; - let akashWallet: SignArbitraryAkashWallet; - - beforeAll(async () => { - testWallet = await DirectSecp256k1HdWallet.fromMnemonic(jwtMnemonic, { - prefix: "akash" - }); - akashWallet = await createSignArbitraryAkashWallet(testWallet); - jwtToken = new JwtToken(akashWallet); - }); - - it.each(jwtClaimsTestCases)("$description", async testCase => { - const { claims, tokenString } = replaceTemplateValues(testCase); - - // For test cases that should fail, we need to validate the payload first - if (testCase.expected.signFail || testCase.expected.verifyFail) { - const validationResult = jwtToken.validatePayload(claims); - expect(validationResult.isValid).toBe(false); - - if (validationResult.isValid) { - throw new Error("Validation should have failed", { cause: testCase }); - } - - return; - } - - // For test cases that should pass, create and verify the token - const token = await jwtToken.createToken(claims as CreateJWTOptions); - const decoded = jwtToken.decodeToken(token); - expect(decoded).toBeDefined(); - - // If the test case has a token string, compare it with the generated token - if (tokenString) { - expect(token).toEqual(tokenString); - } - }); - - it.each(jwtSigningTestCases)("$description", async testCase => { - const [expectedHeader, expectedPayload, expectedSignature] = testCase.tokenString.split("."); - expect(expectedHeader).toBeDefined(); - expect(expectedPayload).toBeDefined(); - expect(expectedSignature).toBeDefined(); - - const signingString = `${expectedHeader}.${expectedPayload}`; - - // Sign using the mock wallet's signArbitrary method - const signResponse = await akashWallet.signArbitrary(akashWallet.address, signingString); - - const signature = Buffer.from(signResponse.signature, "base64url").toString("base64url"); - - if (!testCase.mustFail) { - expect(signature).toBe(expectedSignature); - } else { - expect(signature).not.toBe(expectedSignature); - } - }); -}); diff --git a/packages/jwt/src/jwt-token.ts b/packages/jwt/src/jwt-token.ts deleted file mode 100644 index 1995e249e..000000000 --- a/packages/jwt/src/jwt-token.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { base64UrlEncode } from "./base64"; -import { JwtValidator } from "./jwt-validator"; -import type { JwtTokenPayload } from "./types"; -import type { SignArbitraryAkashWallet } from "./wallet-utils"; - -export class JwtToken { - private validator: JwtValidator; - private wallet: SignArbitraryAkashWallet; - - constructor(wallet: SignArbitraryAkashWallet) { - this.validator = new JwtValidator(); - this.wallet = wallet; - } - - /** - * Creates a new JWT token with ES256K signature using a custom signArbitrary method with the current wallet - * @param options - JWT token options - * @returns The signed JWT token - * @example - * const wallet = await DirectSecp256k1HdWallet.fromMnemonic(jwtMnemonic, { - * prefix: "akash" - * }); - * const akashWallet = await createSignArbitraryAkashWallet(wallet); - * const jwtToken = new JwtToken(akashWallet); - * // OR ON FRONTEND - * const { getAccount, signArbitrary } = useSelectedChain(); - * const { address, pubkey } = await getAccount(); - * const jwt = new JwtToken( - * { - * signArbitrary, - * address, - * pubkey - * } - * ); - * const token = await jwtToken.createToken({ - * version: "v1", - * iss: "https://example.com", - * exp: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now - * iat: Math.floor(Date.now() / 1000), // current timestamp - * }); - * console.log(token); - */ - async createToken(options: CreateJWTOptions): Promise { - const now = Math.floor(Date.now() / 1000); - const inputPayload: JwtTokenPayload = { - iss: options.iss, - exp: options.exp ? options.exp : now + 3600, // Default to 1 hour expiration - nbf: options.nbf || now, - iat: options.iat || now, - jti: options.jti, - version: options.version, - leases: options.leases || { access: "full" } - }; - - const validationResult = this.validatePayload(inputPayload); - if (!validationResult.isValid) { - throw new Error(`Invalid payload: ${validationResult.errors?.join(", ")}`); - } - - const header = base64UrlEncode(JSON.stringify({ alg: "ES256K", typ: "JWT" })); - const stringPayload = base64UrlEncode(JSON.stringify(inputPayload)); - const { signature } = await this.wallet.signArbitrary(this.wallet.address, `${header}.${stringPayload}`); - - const reorderedJWT = `${header}.${stringPayload}.${signature}`; - - return reorderedJWT; - } - - /** - * Decodes a JWT token - * @param token - The JWT token to decode - * @returns The decoded JWT payload - * @throws Error if the token is malformed - */ - decodeToken(token: string): JwtTokenPayload { - const parts = token.split("."); - if (parts.length !== 3) { - throw new Error("Invalid JWT format"); - } - - try { - const [, payload] = parts; - const json = Buffer.from(payload, "base64url").toString("utf8"); - return JSON.parse(json); - } catch (error) { - throw new Error("Failed to decode JWT token", { cause: error }); - } - } - - /** - * Validates a JWT payload against the schema and time-based constraints - * @param payload - The JWT payload to validate - * @returns A boolean indicating whether the payload is valid - */ - public validatePayload(payload: JwtTokenPayload): { isValid: boolean; errors?: string[] } { - const result = this.validator.validateToken(payload); - if (!result.isValid) { - return { isValid: false, errors: result.errors }; - } - - const now = Math.floor(Date.now() / 1000); - const errors: string[] = []; - - if (payload.exp <= now) { - errors.push("Token has expired"); - } - - if (payload.nbf > now) { - errors.push("Token is not yet valid (nbf check failed)"); - } - - return { - isValid: errors.length === 0, - errors: errors.length > 0 ? errors : undefined - }; - } -} - -export interface CreateJWTOptions extends Partial> { - version: JwtTokenPayload["version"]; - iss: JwtTokenPayload["iss"]; -} diff --git a/packages/jwt/src/jwt-validator.spec.ts b/packages/jwt/src/jwt-validator.spec.ts deleted file mode 100644 index 07e91adb3..000000000 --- a/packages/jwt/src/jwt-validator.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { JwtValidator } from "./jwt-validator"; - -describe("JwtValidator", () => { - let validator: JwtValidator; - - beforeEach(() => { - validator = new JwtValidator(); - }); - - it("should validate a valid token", () => { - const validToken = - "eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJha2FzaDFhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejEyMzQ1Njc4OTBhYiIsImlhdCI6MTY1NDAwMDAwMCwiZXhwIjoxNjU0MDAzNjAwLCJuYmYiOjE2NTQwMDAwMDAsInZlcnNpb24iOiJ2MSIsImxlYXNlcyI6eyJhY2Nlc3MiOiJncmFudWxhciIsInBlcm1pc3Npb25zIjpbeyJwcm92aWRlciI6ImFrYXNoMWFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MTIzNDU2Nzg5MGFiIiwiYWNjZXNzIjoic2NvcGVkIiwic2NvcGUiOlsic2VuZC1tYW5pZmVzdCJdfV19fQ.signature"; - const result = validator.validateToken(validToken); - expect(result.isValid).toBe(true); - expect(result.errors.length).toBe(0); - expect(result.decodedToken).toBeDefined(); - }); - - it("should reject a malformed token", () => { - const result = validator.validateToken("not.a.valid.token"); - expect(result.isValid).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - expect(result.errors[0]).toContain("Error during JWT validation"); - }); - - it("should validate required fields in header", () => { - const result = validator.validateToken( - "eyJ0eXAiOiJKV1QifQ.eyJpc3MiOiJha2FzaDFhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejEyMzQ1Njc4OTBhYiIsImlhdCI6MTY1NDAwMDAwMCwiZXhwIjoxNjU0MDAzNjAwLCJuYmYiOjE2NTQwMDAwMDAsInZlcnNpb24iOiJ2MSJ9.signature" - ); - expect(result.isValid).toBe(false); - expect(result.errors).toContain("Missing required field in header: alg"); - }); - - it("should validate required fields in payload", () => { - const result = validator.validateToken("eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ.eyJmb28iOiJiYXIifQ.signature"); - expect(result.isValid).toBe(false); - expect(result.errors).toContain("Missing required field: iss"); - expect(result.errors).toContain("Missing required field: iat"); - expect(result.errors).toContain("Missing required field: exp"); - expect(result.errors).toContain("Missing required field: nbf"); - expect(result.errors).toContain("Missing required field: version"); - expect(result.errors).toContain("Missing required field: leases"); - expect(result.errors).toContain("Additional properties are not allowed"); - }); - - it("should validate leases object when present", () => { - const token = - "eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJha2FzaDFhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejEyMzQ1Njc4OTBhYiIsImlhdCI6MTY1NDAwMDAwMCwiZXhwIjoxNjU0MDAzNjAwLCJuYmYiOjE2NTQwMDAwMDAsInZlcnNpb24iOiJ2MSIsImxlYXNlcyI6e319.signature"; - const result = validator.validateToken(token); - expect(result.isValid).toBe(false); - expect(result.errors).toContain("Missing required field: access"); - }); - - it("should validate granular access requires permissions", () => { - const token = - "eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJha2FzaDFhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejEyMzQ1Njc4OTBhYiIsImlhdCI6MTY1NDAwMDAwMCwiZXhwIjoxNjU0MDAzNjAwLCJuYmYiOjE2NTQwMDAwMDAsInZlcnNpb24iOiJ2MSIsImxlYXNlcyI6eyJhY2Nlc3MiOiJncmFudWxhciJ9fQ.signature"; - const result = validator.validateToken(token); - expect(result.isValid).toBe(false); - expect(result.errors).toContain("Missing required field: permissions"); - }); -}); diff --git a/packages/jwt/src/jwt-validator.ts b/packages/jwt/src/jwt-validator.ts deleted file mode 100644 index c9b79f3a6..000000000 --- a/packages/jwt/src/jwt-validator.ts +++ /dev/null @@ -1,234 +0,0 @@ -import type { ErrorObject, ValidateFunction } from "ajv"; -import Ajv from "ajv"; -import addFormats from "ajv-formats"; - -import { jwtSchemaData } from "./generated/jwt-schema-data"; -import { base64Decode } from "./base64"; -import type { JwtTokenPayload } from "./types"; - -export interface JwtValidationResult { - isValid: boolean; - errors: string[]; - decodedToken?: { - header: Record; - payload: Record; - signature: string; - }; -} - -export class JwtValidator { - private ajv: Ajv; - private compiledSchema: ValidateFunction; - - constructor() { - this.ajv = new Ajv({ - allErrors: true, - strict: false, - strictTypes: false, - strictSchema: false - }); - addFormats(this.ajv); - this.compiledSchema = this.ajv.compile(jwtSchemaData); - } - - /** - * Validate a JWT token against the Akash JWT schema - * @param token The JWT token to validate - * @returns Validation result with errors if any - */ - validateToken(token: string | JwtTokenPayload): JwtValidationResult { - const result: JwtValidationResult = { - isValid: false, - errors: [] - }; - - try { - // Check for empty or null input - if (typeof token === "string" && !token.trim()) { - result.errors.push("Error validating token: Empty token provided"); - return result; - } else if (typeof token !== "string" && (!token || Object.keys(token).length === 0)) { - result.errors.push("Error validating token: Empty payload provided"); - return result; - } - - let payload: Record; - - if (typeof token === "string") { - const parts = token.split(".", 3); - if (parts.length !== 3) { - result.errors.push("Error validating token: Invalid token format"); - return result; - } - - const [headerB64, payloadB64, signature] = parts; - - const header = base64Decode(headerB64); - payload = base64Decode(payloadB64); - - result.decodedToken = { - header, - payload, - signature - }; - - // Validate header - if (!header.alg) { - result.errors.push("Missing required field in header: alg"); - return result; - } - } else { - payload = token; - } - - // Validate payload with the schema - let valid = this.compiledSchema(payload); - - if (!valid) { - result.errors = - this.compiledSchema.errors?.map((error: ErrorObject) => { - if (error.keyword === "required") { - return `Missing required field: ${(error.params as { missingProperty: string }).missingProperty}`; - } - if (error.keyword === "pattern") { - return `Invalid format: ${error.schemaPath.slice(1)} does not match pattern "${(error.params as { pattern: string }).pattern}"`; - } - if (error.keyword === "additionalProperties") { - return "Additional properties are not allowed"; - } - if (error.keyword === "type") { - return `${error.schemaPath.slice(1) || "Field"} should be ${(error.params as { type: string }).type}`; - } - if (error.keyword === "enum") { - return `${error.schemaPath.slice(1) || "Field"} should be one of: ${(error.params as { allowedValues: string[] }).allowedValues.join(", ")}`; - } - return `${error.schemaPath.slice(1) || "Field"}: ${error.message}`; - }) || []; - } - - // Additional validation for granular access - if (payload.leases?.access === "granular") { - if (!payload.leases?.permissions) { - result.errors.push("Missing required field: permissions"); - valid = false; - } else { - // Check for duplicate providers - const providers = new Set(); - for (const perm of payload.leases.permissions) { - if (providers.has(perm.provider)) { - result.errors.push("Duplicate provider in permissions"); - valid = false; - break; - } - providers.add(perm.provider); - - // Validate access type specific rules - if (perm.access === "scoped") { - if (!perm.scope) { - result.errors.push("Missing required field: scope for scoped access"); - valid = false; - } else if (perm.deployments) { - result.errors.push("Deployments not allowed for scoped access"); - valid = false; - } - } else if (perm.access === "granular") { - if (!perm.deployments) { - result.errors.push("Missing required field: deployments for granular access"); - valid = false; - } else if (perm.scope) { - result.errors.push("Scope not allowed for granular access"); - valid = false; - } - } - - // Check for duplicate scopes within each permission - if (perm.scope) { - const scopes = new Set(); - for (const scope of perm.scope) { - if (scopes.has(scope)) { - result.errors.push(`Duplicate scope in permission: ${scope}`); - valid = false; - continue; - } - scopes.add(scope); - } - } - - // Check for duplicate services and validate deployment dependencies - if (perm.deployments) { - for (const deployment of perm.deployments) { - if (!this.validateDeployment(deployment, result)) { - valid = false; - } - } - } - } - } - } else if (payload.leases?.access === "full" && payload.leases?.permissions) { - result.errors.push("Permissions not allowed for full access"); - valid = false; - } - - result.isValid = result.errors.length === 0; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - result.errors.push(`Error during JWT validation: ${errorMessage}`); - } - - return result; - } - - /** - * Validates deployment structure and dependencies - * @param deployment The deployment to validate - * @param result The validation result to update - * @returns Whether the validation passed - */ - private validateDeployment(deployment: any, result: JwtValidationResult): boolean { - let valid = true; - - // Check for duplicate scopes within deployment - const scopes = new Set(); - for (const scope of deployment.scope) { - if (scopes.has(scope)) { - result.errors.push("Duplicate scope in deployment"); - valid = false; - break; - } - scopes.add(scope); - } - - // Validate deployment dependencies - if (deployment.gseq && !deployment.dseq) { - result.errors.push("gseq requires dseq"); - valid = false; - } - if (deployment.oseq && (!deployment.dseq || !deployment.gseq)) { - result.errors.push("oseq requires dseq and gseq"); - valid = false; - } - if (deployment.dseq && !deployment.services) { - result.errors.push("services required when dseq is present"); - valid = false; - } - if (deployment.services && !deployment.dseq) { - result.errors.push("services requires dseq"); - valid = false; - } - - // Check for duplicate services - if (deployment.services) { - const services = new Set(); - for (const service of deployment.services) { - if (services.has(service)) { - result.errors.push("Duplicate service in deployment"); - valid = false; - break; - } - services.add(service); - } - } - - return valid; - } -} diff --git a/packages/jwt/src/test/generated/jwt-claims-test-cases.ts b/packages/jwt/src/test/generated/jwt-claims-test-cases.ts deleted file mode 100644 index 5abdc867b..000000000 --- a/packages/jwt/src/test/generated/jwt-claims-test-cases.ts +++ /dev/null @@ -1,626 +0,0 @@ -// This file contains test cases for JWT claims validation -export const jwtClaimsTestCases = [ - { - description: "sign valid/verify fail with invalid issuer", - tokenString: - "eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ.eyJ2ZXJzaW9uIjoiIiwibGVhc2VzIjp7ImFjY2VzcyI6IiJ9fQ.fQFwGyhJDyF9i_zCX6IwJ43_arjs_1qJmxNSph6t8INMMZ7hBvrzwg0Ym8N06G7O_ZDw0mujQCfmOmR1jegnmA", - claims: {}, - expected: { - error: "token has invalid claims", - signFail: false, - verifyFail: true - } - }, - { - description: "sign valid/verify fail with empty iat", - tokenString: - "eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJha2FzaDFxdWZhM3h6cmYzNHF2d3llamtodm5jNHBzZjQ5ZmZ0YXcwYXNtaCIsInZlcnNpb24iOiIiLCJsZWFzZXMiOnsiYWNjZXNzIjoiIn19.xJmyqk4-2LXPa_l3wQdZhDSsTUatYO8SxBSr_D7_uust0LOFLUqdwIAAX8jpFoWTbbgWN0cQhPNOcBrI3-P9XQ", - claims: { - iss: "{{.Issuer}}" - }, - expected: { - error: "token has invalid claims", - signFail: false, - verifyFail: true - } - }, - { - description: "sign valid/verify against static token string", - tokenString: - "eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJha2FzaDFxdWZhM3h6cmYzNHF2d3llamtodm5jNHBzZjQ5ZmZ0YXcwYXNtaCIsImV4cCI6MjA0NjY2NzEwMywibmJmIjoxNzQ2NjY2MTAzLCJpYXQiOjE3NDY2NjYxMDMsInZlcnNpb24iOiJ2MSIsImxlYXNlcyI6eyJhY2Nlc3MiOiJmdWxsIn19.MZr8b9Q-hcnAUAFbdAVcZpetppP0YDFxJwkRncgE8mEM20woFB2yJuFbfv6YMAUZk8DNORoLaP8hUP8q2FnznQ", - claims: { - iss: "akash1qufa3xzrf34qvwyejkhvnc4psf49fftaw0asmh", - iat: "1746666103", - nbf: "1746666103", - exp: "2046667103", - version: "v1", - leases: { - access: "full" - } - }, - expected: { - signFail: false, - verifyFail: false - } - }, - { - description: "sign valid/verify fail with invalid exp", - claims: { - iss: "{{.Issuer}}", - iat: "{{.IatCurr}}" - }, - expected: { - error: "token has invalid claims", - signFail: false, - verifyFail: true - } - }, - { - description: "sign valid/verify fail with invalid nbf", - claims: { - iss: "{{.Issuer}}", - iat: "{{.IatCurr}}", - exp: "{{.Exp48h}}" - }, - expected: { - error: "token has invalid claims", - signFail: false, - verifyFail: true - } - }, - { - description: "sign valid/verify fail with invalid version", - claims: { - iss: "{{.Issuer}}", - iat: "{{.IatCurr}}", - nbf: "{{.NbfCurr}}", - exp: "{{.Exp48h}}" - }, - expected: { - error: "token has invalid claims", - signFail: false, - verifyFail: true - } - }, - { - description: "sign valid/verify fail with invalid access type", - claims: { - iss: "{{.Issuer}}", - iat: "{{.IatCurr}}", - nbf: "{{.NbfCurr}}", - exp: "{{.Exp48h}}", - version: "v1" - }, - expected: { - error: "token has invalid claims", - signFail: false, - verifyFail: true - } - }, - { - description: "sign valid/verify fail with invalid access type/2", - claims: { - iss: "{{.Issuer}}", - iat: "{{.Iat24h}}", - exp: "{{.Exp48h}}", - nbf: "{{.Nbf24h}}", - version: "v1", - leases: { - access: "unknown" - } - }, - expected: { - error: "token has invalid claims", - signFail: false, - verifyFail: true - } - }, - { - description: "sign valid/verify valid full access", - claims: { - iss: "{{.Issuer}}", - iat: "{{.IatCurr}}", - nbf: "{{.NbfCurr}}", - exp: "{{.Exp48h}}", - version: "v1", - leases: { - access: "full" - } - }, - expected: { - signFail: false, - verifyFail: false - } - }, - { - description: "sign valid/verify fail expired", - claims: { - iss: "{{.Issuer}}", - iat: "{{.IatCurr}}", - nbf: "{{.NbfCurr}}", - exp: "{{.IatCurr}}", - version: "v1", - leases: { - access: "full" - } - }, - expected: { - signFail: false, - verifyFail: true, - bypassSchemaValidation: true - } - }, - { - description: "sign valid/verify fail not yet valid", - claims: { - iss: "{{.Issuer}}", - iat: "{{.IatCurr}}", - nbf: "{{.Nbf24h}}", - exp: "{{.Exp48h}}", - version: "v1", - leases: { - access: "full" - } - }, - expected: { - signFail: false, - verifyFail: true, - bypassSchemaValidation: true - } - }, - { - description: "sign valid/verify fail granular access", - claims: { - iss: "{{.Issuer}}", - iat: "{{.IatCurr}}", - nbf: "{{.NbfCurr}}", - exp: "{{.Exp48h}}", - version: "v1", - leases: { - access: "granular" - } - }, - expected: { - error: "token has invalid claims", - signFail: false, - verifyFail: true - } - }, - { - description: "sign valid/verify fail granular access/specific provider/missing access", - claims: { - iss: "{{.Issuer}}", - iat: "{{.IatCurr}}", - nbf: "{{.NbfCurr}}", - exp: "{{.Exp48h}}", - version: "v1", - leases: { - access: "granular", - permissions: [ - { - provider: "{{.Provider}}" - } - ] - } - }, - expected: { - error: "token has invalid claims", - signFail: false, - verifyFail: true - } - }, - { - description: "sign valid/verify pass/specific provider/full access", - claims: { - iss: "{{.Issuer}}", - iat: "{{.IatCurr}}", - nbf: "{{.NbfCurr}}", - exp: "{{.Exp48h}}", - version: "v1", - leases: { - access: "granular", - permissions: [ - { - provider: "{{.Provider}}", - access: "full" - } - ] - } - }, - expected: { - error: "token has invalid claims", - signFail: false, - verifyFail: false - } - }, - { - description: "sign valid/verify fail/specific provider/scoped access", - claims: { - iss: "{{.Issuer}}", - iat: "{{.IatCurr}}", - nbf: "{{.NbfCurr}}", - exp: "{{.Exp48h}}", - version: "v1", - leases: { - access: "granular", - permissions: [ - { - provider: "{{.Provider}}", - access: "scoped" - } - ] - } - }, - expected: { - error: "token has invalid claims", - signFail: false, - verifyFail: true - } - }, - { - description: "sign valid/verify pass/specific provider/scoped access", - claims: { - iss: "{{.Issuer}}", - iat: "{{.IatCurr}}", - nbf: "{{.NbfCurr}}", - exp: "{{.Exp48h}}", - version: "v1", - leases: { - access: "granular", - permissions: [ - { - provider: "{{.Provider}}", - access: "scoped", - scope: ["send-manifest"] - } - ] - } - }, - expected: { - error: "token has invalid claims", - signFail: false, - verifyFail: false - } - }, - { - description: "sign valid/verify fail/specific provider/scoped access/duplicate", - claims: { - iss: "{{.Issuer}}", - iat: "{{.IatCurr}}", - nbf: "{{.NbfCurr}}", - exp: "{{.Exp48h}}", - version: "v1", - leases: { - access: "granular", - permissions: [ - { - provider: "{{.Provider}}", - access: "scoped", - scope: ["send-manifest", "send-manifest"] - } - ] - } - }, - expected: { - error: "token has invalid claims", - signFail: false, - verifyFail: true - } - }, - { - description: "sign valid/verify fail/specific provider/granular access with scope", - claims: { - iss: "{{.Issuer}}", - iat: "{{.IatCurr}}", - nbf: "{{.NbfCurr}}", - exp: "{{.Exp48h}}", - version: "v1", - leases: { - access: "granular", - permissions: [ - { - provider: "{{.Provider}}", - access: "granular", - scope: ["send-manifest"] - } - ] - } - }, - expected: { - error: "token has invalid claims", - signFail: false, - verifyFail: true - } - }, - { - description: "sign valid/verify fail granular access/duplicate provider", - claims: { - iss: "{{.Issuer}}", - iat: "{{.IatCurr}}", - nbf: "{{.NbfCurr}}", - exp: "{{.Exp48h}}", - version: "v1", - leases: { - access: "granular", - permissions: [ - { - provider: "{{.Provider}}", - access: "scoped", - scope: ["send-manifest"] - }, - { - provider: "{{.Provider}}", - access: "scoped", - scope: ["send-manifest"] - } - ] - } - }, - expected: { - error: "token has invalid claims", - signFail: false, - verifyFail: true, - bypassSchemaValidation: true - } - }, - { - description: "sign valid/verify fail granular access/specific provider/unknown scope", - claims: { - iss: "{{.Issuer}}", - iat: "{{.IatCurr}}", - nbf: "{{.NbfCurr}}", - exp: "{{.Exp48h}}", - version: "v1", - leases: { - access: "granular", - permissions: [ - { - provider: "{{.Provider}}", - access: "scoped", - scope: ["unknown"] - } - ] - } - }, - expected: { - error: "token has invalid claims", - signFail: false, - verifyFail: true - } - }, - { - description: "sign valid/verify fail granular access/specific provider/deployment/missing scope", - claims: { - iss: "{{.Issuer}}", - iat: "{{.IatCurr}}", - nbf: "{{.NbfCurr}}", - exp: "{{.Exp48h}}", - version: "v1", - leases: { - access: "granular", - permissions: [ - { - provider: "{{.Provider}}", - access: "granular", - deployments: [ - { - services: ["web"] - } - ] - } - ] - } - }, - expected: { - signFail: false, - verifyFail: true - } - }, - { - description: "sign valid/verify fail granular access/specific provider/deployment/duplicate scope", - claims: { - iss: "{{.Issuer}}", - iat: "{{.IatCurr}}", - nbf: "{{.NbfCurr}}", - exp: "{{.Exp48h}}", - version: "v1", - leases: { - access: "granular", - permissions: [ - { - provider: "{{.Provider}}", - access: "granular", - deployments: [ - { - scope: ["send-manifest", "send-manifest"] - } - ] - } - ] - } - }, - expected: { - signFail: false, - verifyFail: true - } - }, - { - description: "sign valid/verify fail granular access/specific provider/deployment/invalid scope", - claims: { - iss: "{{.Issuer}}", - iat: "{{.IatCurr}}", - nbf: "{{.NbfCurr}}", - exp: "{{.Exp48h}}", - version: "v1", - leases: { - access: "granular", - permissions: [ - { - provider: "{{.Provider}}", - access: "granular", - deployments: [ - { - scope: ["unknown"] - } - ] - } - ] - } - }, - expected: { - signFail: false, - verifyFail: true - } - }, - { - description: "sign valid/verify fail granular access/specific provider/deployment/missing dseq", - claims: { - iss: "{{.Issuer}}", - iat: "{{.IatCurr}}", - nbf: "{{.NbfCurr}}", - exp: "{{.Exp48h}}", - version: "v1", - leases: { - access: "granular", - permissions: [ - { - provider: "{{.Provider}}", - access: "granular", - deployments: [ - { - scope: ["send-manifest"] - } - ] - } - ] - } - }, - expected: { - signFail: false, - verifyFail: true - } - }, - { - description: "sign valid/verify fail granular access/specific provider/deployment/no services", - claims: { - iss: "{{.Issuer}}", - iat: "{{.IatCurr}}", - nbf: "{{.NbfCurr}}", - exp: "{{.Exp48h}}", - version: "v1", - leases: { - access: "granular", - permissions: [ - { - provider: "{{.Provider}}", - access: "granular", - deployments: [ - { - scope: ["send-manifest"], - dseq: 1 - } - ] - } - ] - } - }, - expected: { - signFail: false, - verifyFail: true - } - }, - { - description: "sign valid/verify pass granular access/specific provider/deployment", - claims: { - iss: "{{.Issuer}}", - iat: "{{.IatCurr}}", - nbf: "{{.NbfCurr}}", - exp: "{{.Exp48h}}", - version: "v1", - leases: { - access: "granular", - permissions: [ - { - provider: "{{.Provider}}", - access: "granular", - deployments: [ - { - scope: ["send-manifest"], - dseq: 1, - services: ["web"] - } - ] - } - ] - } - }, - expected: { - signFail: false, - verifyFail: false - } - }, - { - description: "sign valid/verify fail granular access/specific provider/deployment/duplicate service", - claims: { - iss: "{{.Issuer}}", - iat: "{{.IatCurr}}", - nbf: "{{.NbfCurr}}", - exp: "{{.Exp48h}}", - version: "v1", - leases: { - access: "granular", - permissions: [ - { - provider: "{{.Provider}}", - access: "granular", - deployments: [ - { - scope: ["send-manifest"], - dseq: 1, - services: ["web", "web"] - } - ] - } - ] - } - }, - expected: { - error: "token has invalid claims", - signFail: false, - verifyFail: true, - bypassSchemaValidation: true - } - }, - { - description: "sign valid/verify fail granular access/specific provider/deployment/oseq", - claims: { - iss: "{{.Issuer}}", - iat: "{{.IatCurr}}", - nbf: "{{.NbfCurr}}", - exp: "{{.Exp48h}}", - version: "v1", - leases: { - access: "granular", - permissions: [ - { - provider: "{{.Provider}}", - access: "granular", - deployments: [ - { - scope: ["send-manifest"], - dseq: 1, - oseq: 1, - services: ["web", "web"] - } - ] - } - ] - } - }, - expected: { - error: "token has invalid claims", - signFail: false, - verifyFail: true - } - } -]; diff --git a/packages/jwt/src/test/generated/jwt-mnemonic.ts b/packages/jwt/src/test/generated/jwt-mnemonic.ts deleted file mode 100644 index 3eab64ced..000000000 --- a/packages/jwt/src/test/generated/jwt-mnemonic.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file contains the test mnemonic for JWT signing -export const jwtMnemonic = - "addict worth drip violin note olive final pelican perfect feel goat game horror brass fence deposit dutch improve setup stuff frost spirit smart alien"; diff --git a/packages/jwt/src/test/generated/jwt-signing-test-cases.ts b/packages/jwt/src/test/generated/jwt-signing-test-cases.ts deleted file mode 100644 index a27ea4339..000000000 --- a/packages/jwt/src/test/generated/jwt-signing-test-cases.ts +++ /dev/null @@ -1,30 +0,0 @@ -// This file contains test cases for JWT signing validation -export const jwtSigningTestCases = [ - { - description: "ES256K - Valid Signature", - tokenString: - "eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJiYXIifQ.uq2X8CtBrg-fPvkJ5Dl-AHWQ1HPVnZfA1o0azRlHEBkE7YzOdr44UWmlkavjrl3lMHr4jhROugXi8cjrrZ2Kzw", - expected: { - alg: "ES256K", - claims: { - issuer: "bar" - } - }, - mustFail: false - }, - { - description: "ES256K - Invalid Signature", - tokenString: - "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJiYXIifQ.MEQCIHoSJnmGlPaVQDqacx_2XlXEhhqtWceVopjomc2PJLtdAiAUTeGPoNYxZw0z8mgOnnIcjoxRuNDVZvybRZF3wR1l8W", - expected: { - alg: "ES256K", - claims: { - issuer: "bar" - } - }, - claims: { - issuer: "bar" - }, - mustFail: true - } -]; diff --git a/packages/jwt/src/test/seeders/akash-address.seeder.ts b/packages/jwt/src/test/seeders/akash-address.seeder.ts deleted file mode 100644 index b87a595ec..000000000 --- a/packages/jwt/src/test/seeders/akash-address.seeder.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { faker } from "@faker-js/faker"; - -export function createAkashAddress(): string { - return `akash1${faker.string.alphanumeric({ length: 38, casing: "lower" })}`; -} diff --git a/packages/jwt/src/test/test-utils.ts b/packages/jwt/src/test/test-utils.ts deleted file mode 100644 index a3820fc1a..000000000 --- a/packages/jwt/src/test/test-utils.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { JwtTokenPayload } from "../types"; -import type { jwtClaimsTestCases } from "./generated/jwt-claims-test-cases"; -import { createAkashAddress } from "./seeders/akash-address.seeder"; - -const ONE_DAY_IN_SECONDS = 60 * 60 * 24; -const TWO_DAYS_IN_SECONDS = 2 * ONE_DAY_IN_SECONDS; - -/** - * Replaces template values in JWT test cases with actual values - * - * Supports the following template patterns: - * - {{.Issuer}} - Replaced with a generated Akash address for the issuer - * - {{.Provider}} - Replaced with a generated Akash address for the provider - * - {{.IatCurr}} - Replaced with the current timestamp - * - {{.Iat24h}} - Replaced with a timestamp 24 hours in the past - * - {{.NbfCurr}} - Replaced with the current timestamp - * - {{.Nbf24h}} - Replaced with a timestamp 24 hours in the past - * - {{.Exp48h}} - Replaced with a timestamp 48 hours in the future - * @param testCase - The test case containing template values - * @returns The test case with template values replaced - */ -export function replaceTemplateValues(testCase: (typeof jwtClaimsTestCases)[0]) { - const now = Math.floor(Date.now() / 1000); - const issuer = createAkashAddress(); - const provider = createAkashAddress(); - - const claims = { ...testCase.claims } as any; - if (claims.iss === "{{.Issuer}}") claims.iss = issuer; - if (claims.iat === "{{.IatCurr}}") claims.iat = now; - if (claims.iat === "{{.Iat24h}}") claims.iat = now + ONE_DAY_IN_SECONDS; - if (claims.nbf === "{{.NbfCurr}}") claims.nbf = now; - if (claims.nbf === "{{.Nbf24h}}") claims.nbf = now + ONE_DAY_IN_SECONDS; - if (claims.exp === "{{.Exp48h}}") claims.exp = now + TWO_DAYS_IN_SECONDS; - - // Convert string timestamps to numbers - if (typeof claims.iat === "string") claims.iat = parseInt(claims.iat, 10); - if (typeof claims.exp === "string") claims.exp = parseInt(claims.exp, 10); - if (typeof claims.nbf === "string") claims.nbf = parseInt(claims.nbf, 10); - - // Replace provider address in permissions if present - if (claims.leases && Array.isArray(claims.leases.permissions) && claims.leases.permissions.length > 0) { - claims.leases.permissions = claims.leases.permissions.map((perm: { provider: string; [key: string]: any }) => ({ - ...perm, - provider: perm.provider === "{{.Provider}}" ? provider : perm.provider - })); - } - - return { ...testCase, claims: claims as JwtTokenPayload }; -} diff --git a/packages/jwt/src/types.ts b/packages/jwt/src/types.ts deleted file mode 100644 index a2289c289..000000000 --- a/packages/jwt/src/types.ts +++ /dev/null @@ -1,67 +0,0 @@ -export type AccessScope = "send-manifest" | "get-manifest" | "logs" | "shell" | "events" | "status" | "restart" | "hostname-migrate" | "ip-migrate"; - -export interface JwtTokenPayload { - /** Version of the JWT specification (currently fixed at v1). */ - version: "v1"; - /** Akash address of the lease(s) owner, e.g., akash1abcd... (44 characters) */ - iss: string; - /** Token issuance timestamp as Unix time (seconds since 1970-01-01T00:00:00Z). Should be <= exp and >= nbf. */ - iat: number; - /** Not valid before timestamp as Unix time (seconds since 1970-01-01T00:00:00Z). Should be <= iat. */ - nbf: number; - /** Expiration timestamp as Unix time (seconds since 1970-01-01T00:00:00Z). Should be >= iat. */ - exp: number; - /** Unique identifier for the JWT, used to prevent token reuse. */ - jti?: string; - /** Access level for the lease: 'full' for unrestricted access to all actions, 'granular' for provider-specific permissions. */ - leases: FullAccess | GranularAccess; -} - -interface FullAccess { - access: "full"; - /** Global list of permitted actions across all owned leases (no duplicates). */ - scope?: AccessScope[]; -} - -interface GranularAccess { - access: "granular"; - /** Defines provider-specific permissions. */ - permissions: LeasePermission[]; -} - -export interface JWTHeader { - alg: string; - typ: string; -} - -export type LeasePermission = FullAccessPermission | ScopedAccessPermission | GranularAccessPermission; - -interface BaseLeasePermission { - /** Provider address, e.g., akash1xyz... (44 characters). */ - provider: string; -} - -interface FullAccessPermission extends BaseLeasePermission { - access: "full"; -} - -interface ScopedAccessPermission extends BaseLeasePermission { - access: "scoped"; - scope: AccessScope[]; -} - -interface GranularAccessPermission extends BaseLeasePermission { - access: "granular"; - deployments: Array<{ - /** Deployment sequence number. */ - dseq: number; - /** Deployment-level list of permitted actions (no duplicates). */ - scope: AccessScope[]; - /** Group sequence number (requires dseq). */ - gseq?: number; - /** Order sequence number (requires dseq and gseq). */ - oseq?: number; - /** List of service names (requires dseq). */ - services?: string[]; - }>; -} diff --git a/packages/jwt/src/wallet-utils.ts b/packages/jwt/src/wallet-utils.ts deleted file mode 100644 index e64f6f959..000000000 --- a/packages/jwt/src/wallet-utils.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { encodeSecp256k1Signature, type StdSignature } from "@cosmjs/amino"; -import { Bip39, EnglishMnemonic, Secp256k1, sha256, Slip10, Slip10Curve, stringToPath } from "@cosmjs/crypto"; -import type { DirectSecp256k1HdWallet, DirectSecp256k1HdWalletOptions } from "@cosmjs/proto-signing"; - -export interface SignArbitraryAkashWallet { - pubkey: Uint8Array; - address: string; - signArbitrary: (signer: string, data: string | Uint8Array, accountIndex?: number) => Promise; -} - -const BASE_HD_PATH = "m/44'/118'/0'/0/"; - -/** - * Create a custom wallet that can sign arbitrary data - * @param wallet - The DirectSecp256k1HdWallet instance to use for signing - * @returns An Akash Wallet interface implementation - */ -export async function createSignArbitraryAkashWallet(wallet: DirectSecp256k1HdWallet, accountIndex: number = 0): Promise { - const [account] = await wallet.getAccounts(); - - return { - pubkey: account.pubkey, - address: account.address, - signArbitrary: async (signer: string, data: string | Uint8Array): Promise => { - const message = typeof data === "string" ? new TextEncoder().encode(data) : data; - const hashedMessage = sha256(message); - const seed = await fromMnemonic(wallet.mnemonic); - const { privkey } = Slip10.derivePath(Slip10Curve.Secp256k1, seed, stringToPath(`${BASE_HD_PATH}${accountIndex}`)); - const signature = await Secp256k1.createSignature(hashedMessage, privkey); - const signatureBytes = new Uint8Array([...signature.r(32), ...signature.s(32)]); - const stdSignature = encodeSecp256k1Signature(account.pubkey, signatureBytes); - - return { - ...stdSignature, - signature: Buffer.from(signatureBytes).toString("base64url") - }; - } - }; -} - -async function fromMnemonic(mnemonic: string, options: Partial = {}): Promise { - const mnemonicChecked = new EnglishMnemonic(mnemonic); - const seed = await Bip39.mnemonicToSeed(mnemonicChecked, options.bip39Password); - return seed; -} diff --git a/packages/jwt/tsconfig.build.json b/packages/jwt/tsconfig.build.json deleted file mode 100644 index dad3ae5cd..000000000 --- a/packages/jwt/tsconfig.build.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": ".", - "noImplicitAny": true, - "strict": true, - "moduleResolution": "node", - "module": "CommonJS" - }, - "extends": "@akashnetwork/dev-config/tsconfig.base-node.json" -} diff --git a/packages/jwt/tsconfig.json b/packages/jwt/tsconfig.json deleted file mode 100644 index f37090002..000000000 --- a/packages/jwt/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "exclude": ["node_modules", "dist", "scripts"], - "extends": "./tsconfig.build.json", - "include": ["src/**/*"] -}