Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
Expand Down
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@
"dependencies": {
"@akashnetwork/akash-api": "^1.3.0",
"@akashnetwork/akashjs": "^0.11.1",
"@akashnetwork/chain-sdk": "^1.0.0-alpha.3",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Pin the alpha version more strictly.

Using ^1.0.0-alpha.3 allows npm to install any version >= 1.0.0-alpha.3 and < 2.0.0, including future alpha releases that may introduce breaking changes. For pre-release versions, use exact pinning or tilde (~) to limit updates to patch-level changes only.

Apply this diff to pin the version more strictly:

-    "@akashnetwork/chain-sdk": "^1.0.0-alpha.3",
+    "@akashnetwork/chain-sdk": "1.0.0-alpha.3",

Alternatively, if you want to allow patch-level updates:

-    "@akashnetwork/chain-sdk": "^1.0.0-alpha.3",
+    "@akashnetwork/chain-sdk": "~1.0.0-alpha.3",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@akashnetwork/chain-sdk": "^1.0.0-alpha.3",
"@akashnetwork/chain-sdk": "1.0.0-alpha.3",
Suggested change
"@akashnetwork/chain-sdk": "^1.0.0-alpha.3",
"@akashnetwork/chain-sdk": "~1.0.0-alpha.3",
🤖 Prompt for AI Agents
In apps/api/package.json at line 42, the dependency is currently
"^1.0.0-alpha.3" which allows unintended pre-release upgrades; remove the caret
to pin exactly ("1.0.0-alpha.3") or, if you want to allow only patch-level
pre-release updates, replace the caret with a tilde ("~1.0.0-alpha.3"); update
the version string accordingly and run npm install / yarn to update lockfile.

"@akashnetwork/database": "*",
"@akashnetwork/env-loader": "*",
"@akashnetwork/http-sdk": "*",
"@akashnetwork/jwt": "*",
"@akashnetwork/logging": "*",
"@akashnetwork/net": "*",
"@akashnetwork/react-query-sdk": "*",
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
});
Comment on lines +37 to +54
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Drop the as any cast in favor of typed setup wiring

Casting to any at Line 40 violates our TypeScript guideline (“Never use type any or cast to type any”). You can avoid the cast altogether by letting setup() initialize authService.currentUser when a user is provided, and then remove both the manual assignment in the happy-path test and the undefined as any line here; setup() without a user will already leave currentUser undefined. For example:

-      const user = UserSeeder.create();
-      const { controller, authService, userWalletRepository, providerJwtTokenService, jwtToken, wallet } = setup({ user });
-
-      authService.currentUser = user;
+      const user = UserSeeder.create();
+      const { controller, authService, userWalletRepository, providerJwtTokenService, jwtToken, wallet } = setup({ user });-      const { controller, authService } = setup();
-
-      authService.currentUser = undefined as any;
+      const { controller, authService } = setup();
…
   function setup(input?: { user?: UserOutput }) {
     const authService = mock<AuthService>();
+    if (input?.user) {
+      authService.currentUser = input.user;
+    }

That keeps the tests readable and compliant with the repo rules. As per coding guidelines.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/api/src/provider/controllers/jwt-token/jwt-token.controller.spec.ts
around lines 37 to 54, the test uses an `undefined as any` cast to
force-authService.currentUser to undefined; remove the `as any` cast and instead
update the `setup()` helper to accept an optional `user` parameter and
initialize `authService.currentUser` accordingly (undefined when no user
passed). Then change the unauthenticated test to call `setup()` with no user and
remove the manual `authService.currentUser` assignment, and in the
wallet-missing test call `setup({ user })` and remove the redundant
`authService.currentUser = user` assignment; ensure `setup()` returns correctly
typed authService so tests compile without any `any` casts.

});

function setup(input?: { user?: UserOutput }) {
const authService = mock<AuthService>();
const userWalletRepository = mock<UserWalletRepository>();
const providerJwtTokenService = mock<ProviderJwtTokenService>();

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"]
}
};
}
});
Original file line number Diff line number Diff line change
@@ -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<CreateJwtTokenResponse> {
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
};
}
}
56 changes: 56 additions & 0 deletions apps/api/src/provider/http-schemas/jwt-token.schema.ts
Original file line number Diff line number Diff line change
@@ -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<JwtTokenPayload["leases"]>;

export const CreateJwtTokenRequestSchema = z.object({
ttl: z.number().int().positive(),
leases: LeasesSchema
});

export type CreateJwtTokenRequest = z.infer<typeof CreateJwtTokenRequestSchema>;

export const CreateJwtTokenResponseSchema = z.object({
token: z.string()
});
export type CreateJwtTokenResponse = z.infer<typeof CreateJwtTokenResponseSchema>;
7 changes: 6 additions & 1 deletion apps/api/src/provider/providers/jwt.provider.ts
Original file line number Diff line number Diff line change
@@ -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<JWTModule> = "JWT_MODULE";

Expand Down
1 change: 1 addition & 0 deletions apps/api/src/provider/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
46 changes: 46 additions & 0 deletions apps/api/src/provider/routes/jwt-token/jwt-token.router.ts
Original file line number Diff line number Diff line change
@@ -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);
}
);
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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),
Expand All @@ -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", () => {
Expand Down Expand Up @@ -83,12 +83,12 @@ describe(ProviderJwtTokenService.name, () => {
signArbitrary: jest.fn().mockResolvedValue({ signature: "test-signature" })
};

const jwtToken = mock<JwtToken>();
jwtToken.createToken.mockImplementation(() => Promise.resolve(jwtTokenValue));
const jwtToken = mock<JwtTokenManager>();
jwtToken.generateToken.mockImplementation(() => Promise.resolve(jwtTokenValue));

const jwtModule = mock<JWTModule>({
createSignArbitraryAkashWallet: jest.fn(async () => akashWallet),
JwtToken: jest.fn(() => jwtToken)
JwtTokenManager: jest.fn(() => jwtToken)
});

const billingConfigService = mockConfigService<BillingConfigService>({
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
};

Expand All @@ -22,6 +22,8 @@ type GenerateJwtTokenParams = {
ttl?: number;
};

type AccessScope = Extract<Extract<JwtTokenPayload["leases"], { access: "granular" }>["permissions"][number], { access: "scoped" }>["scope"][number];

@singleton()
export class ProviderJwtTokenService {
constructor(
Expand All @@ -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,
Expand All @@ -48,10 +50,10 @@ export class ProviderJwtTokenService {
@Memoize({ ttlInSeconds: minutesToSeconds(5) })
private async getJwtToken(walletId: number): Promise<JwtTokenWithAddress> {
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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Remove unsafe as any cast.

The as any cast violates the coding guidelines and bypasses TypeScript's type safety. Define the proper type for the wallet instance or update the type signature of createSignArbitraryAkashWallet to accept the actual return type of wallet.getInstance().

As per coding guidelines: "Never use type any or cast to type any. Always define the proper TypeScript types."

Apply this approach to fix the type safety:

-    const akashWallet = await this.jwtModule.createSignArbitraryAkashWallet((await wallet.getInstance()) as any, walletId);
+    const walletInstance = await wallet.getInstance();
+    const akashWallet = await this.jwtModule.createSignArbitraryAkashWallet(walletInstance, walletId);

Then ensure createSignArbitraryAkashWallet has the correct type signature to accept DirectSecp256k1HdWallet or the appropriate wallet type.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const akashWallet = await this.jwtModule.createSignArbitraryAkashWallet((await wallet.getInstance()) as any, walletId);
// before:
- const akashWallet = await this.jwtModule.createSignArbitraryAkashWallet((await wallet.getInstance()) as any, walletId);
// after:
const walletInstance = await wallet.getInstance();
const akashWallet = await this.jwtModule.createSignArbitraryAkashWallet(walletInstance, walletId);
🤖 Prompt for AI Agents
In
apps/api/src/provider/services/provider-jwt-token/provider-jwt-token.service.ts
around line 53, remove the unsafe `as any` cast on the result of
`wallet.getInstance()` by giving `wallet.getInstance()` a concrete return type
(e.g., DirectSecp256k1HdWallet or the actual wallet interface) and updating the
signature of `createSignArbitraryAkashWallet` in jwtModule to accept that exact
type; replace the cast with the properly typed value, import or declare the
wallet type where needed, and run TypeScript compilation to ensure no further
mismatches.

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"] {
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/rest-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
providerDeploymentsRouter,
providerEarningsRouter,
providerGraphDataRouter,
providerJwtTokenRouter,
providerRegionsRouter,
providersRouter,
providerVersionsRouter
Expand Down Expand Up @@ -140,6 +141,7 @@ const openApiHonoHandlers: OpenApiHonoHandler[] = [
providerVersionsRouter,
providerGraphDataRouter,
providerDeploymentsRouter,
providerJwtTokenRouter,
graphDataRouter,
dashboardDataRouter,
networkCapacityRouter,
Expand Down
Loading