diff --git a/packages/zitadel-client2/.eslintrc.cjs b/packages/zitadel-client2/.eslintrc.cjs new file mode 100644 index 00000000000..8e247ab3c27 --- /dev/null +++ b/packages/zitadel-client2/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: ["zitadel"], +}; diff --git a/packages/zitadel-client2/package.json b/packages/zitadel-client2/package.json new file mode 100644 index 00000000000..98770c1b5a0 --- /dev/null +++ b/packages/zitadel-client2/package.json @@ -0,0 +1,55 @@ +{ + "name": "@zitadel/client2", + "version": "0.0.0", + "license": "MIT", + "private": true, + "publishConfig": { + "access": "public" + }, + "type": "module", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs", + "types": "./dist/index.d.ts" + }, + "./v1": { + "import": "./dist/v1.js", + "require": "./dist/v1.cjs", + "types": "./dist/v1.d.ts" + }, + "./v2beta": { + "import": "./dist/v2beta.js", + "require": "./dist/v2beta.cjs", + "types": "./dist/v2beta.d.ts" + }, + "./v3alpha": { + "import": "./dist/v3alpha.js", + "require": "./dist/v3alpha.cjs", + "types": "./dist/v3alpha.d.ts" + } + }, + "files": [ + "dist/**" + ], + "sideEffects": false, + "scripts": { + "build": "tsup", + "test": "pnpm test:unit", + "test:watch": "pnpm test:unit:watch", + "test:unit": "vitest", + "test:unit:watch": "vitest --watch", + "dev": "tsup --watch --dts", + "lint": "eslint \"src/**/*.ts*\"", + "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" + }, + "dependencies": { + "@zitadel/proto": "workspace:*", + "@bufbuild/protobuf": "^1.8.0", + "@connectrpc/connect": "^1.4.0" + }, + "devDependencies": { + "@zitadel/tsconfig": "workspace:*", + "eslint-config-zitadel": "workspace:*" + } +} diff --git a/packages/zitadel-client2/src/helpers.ts b/packages/zitadel-client2/src/helpers.ts new file mode 100644 index 00000000000..fbae0ed0a95 --- /dev/null +++ b/packages/zitadel-client2/src/helpers.ts @@ -0,0 +1,8 @@ +import { createPromiseClient, Transport } from "@connectrpc/connect"; +import type { ServiceType } from "@bufbuild/protobuf"; + +export function createClientFor( + service: TService, +) { + return (transport: Transport) => createPromiseClient(service, transport); +} diff --git a/packages/zitadel-client2/src/index.ts b/packages/zitadel-client2/src/index.ts new file mode 100644 index 00000000000..b1dcfa8fe8c --- /dev/null +++ b/packages/zitadel-client2/src/index.ts @@ -0,0 +1,2 @@ +export { NewAuthorizationBearerInterceptor } from "./interceptors"; +export type { PartialMessage, PlainMessage } from "@bufbuild/protobuf"; diff --git a/packages/zitadel-client2/src/interceptors.test.ts b/packages/zitadel-client2/src/interceptors.test.ts new file mode 100644 index 00000000000..dd70a2c3f26 --- /dev/null +++ b/packages/zitadel-client2/src/interceptors.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, test, vitest } from "vitest"; +import { Int32Value, MethodKind, StringValue } from "@bufbuild/protobuf"; +import { createRouterTransport, HandlerContext } from "@connectrpc/connect"; +import { NewAuthorizationBearerInterceptor } from "./interceptors"; + +const TestService = { + typeName: "handwritten.TestService", + methods: { + unary: { + name: "Unary", + I: Int32Value, + O: StringValue, + kind: MethodKind.Unary, + }, + }, +} as const; + +describe("NewAuthorizationBearerInterceptor", () => { + const transport = { + interceptors: [NewAuthorizationBearerInterceptor("mytoken")], + }; + + test("injects the authorization token", async () => { + const handler = vitest.fn( + (request: Int32Value, context: HandlerContext) => { + return { value: request.value.toString() }; + }, + ); + + const service = createRouterTransport( + ({ service }) => { + service(TestService, { unary: handler }); + }, + { transport }, + ); + + await service.unary( + TestService, + TestService.methods.unary, + undefined, + undefined, + {}, + { value: 9001 }, + ); + + expect(handler).toBeCalled(); + expect(handler.mock.calls[0][1].requestHeader.get("Authorization")).toBe( + "Bearer mytoken", + ); + }); + + test("do not overwrite the previous authorization token", async () => { + const handler = vitest.fn( + (request: Int32Value, context: HandlerContext) => { + return { value: request.value.toString() }; + }, + ); + + const service = createRouterTransport( + ({ service }) => { + service(TestService, { unary: handler }); + }, + { transport }, + ); + + await service.unary( + TestService, + TestService.methods.unary, + undefined, + undefined, + { Authorization: "Bearer somethingelse" }, + { value: 9001 }, + ); + + expect(handler).toBeCalled(); + expect(handler.mock.calls[0][1].requestHeader.get("Authorization")).toBe( + "Bearer somethingelse", + ); + }); +}); diff --git a/packages/zitadel-client2/src/interceptors.ts b/packages/zitadel-client2/src/interceptors.ts new file mode 100644 index 00000000000..9d719c7d70b --- /dev/null +++ b/packages/zitadel-client2/src/interceptors.ts @@ -0,0 +1,16 @@ +import type { Interceptor } from "@connectrpc/connect"; + +/** + * Creates an interceptor that adds an Authorization header with a Bearer token. + * @param token + */ +export function NewAuthorizationBearerInterceptor(token: string): Interceptor { + return (next) => (req) => { + // TODO: I am not what is the intent of checking for the Authorization header + // and setting it if it is not present. + if (!req.header.get("Authorization")) { + req.header.set("Authorization", `Bearer ${token}`); + } + return next(req); + }; +} diff --git a/packages/zitadel-client2/src/v1.ts b/packages/zitadel-client2/src/v1.ts new file mode 100644 index 00000000000..5aef4507552 --- /dev/null +++ b/packages/zitadel-client2/src/v1.ts @@ -0,0 +1,11 @@ +import { createClientFor } from "./helpers"; + +import { AdminService } from "@zitadel/proto/zitadel/admin_connect"; +import { AuthService } from "@zitadel/proto/zitadel/auth_connect"; +import { ManagementService } from "@zitadel/proto/zitadel/management_connect"; +import { SystemService } from "@zitadel/proto/zitadel/system_connect"; + +export const createAdminServiceClient = createClientFor(AdminService); +export const createAuthServiceClient = createClientFor(AuthService); +export const createManagementServiceClient = createClientFor(ManagementService); +export const createSystemServiceClient = createClientFor(SystemService); diff --git a/packages/zitadel-client2/src/v2beta.ts b/packages/zitadel-client2/src/v2beta.ts new file mode 100644 index 00000000000..ba718764a0c --- /dev/null +++ b/packages/zitadel-client2/src/v2beta.ts @@ -0,0 +1,28 @@ +import type { PartialMessage } from "@bufbuild/protobuf"; + +import { createClientFor } from "./helpers"; +import { UserService } from "@zitadel/proto/zitadel/user/v2beta/user_service_connect"; +import { SettingsService } from "@zitadel/proto/zitadel/settings/v2beta/settings_service_connect"; +import { SessionService } from "@zitadel/proto/zitadel/session/v2beta/session_service_connect"; +import { OIDCService } from "@zitadel/proto/zitadel/oidc/v2beta/oidc_service_connect"; +import { OrganizationService } from "@zitadel/proto/zitadel/org/v2beta/org_service_connect"; +import { FeatureService } from "@zitadel/proto/zitadel/feature/v2beta/feature_service_connect"; +import type { RequestContext } from "@zitadel/proto/zitadel/object/v2beta/object_pb"; + +export const createUserServiceClient = createClientFor(UserService); +export const createSettingsServiceClient = createClientFor(SettingsService); +export const createSessionServiceClient = createClientFor(SessionService); +export const createOIDCServiceClient = createClientFor(OIDCService); +export const createOrganizationServiceClient = + createClientFor(OrganizationService); +export const createFeatureServiceClient = createClientFor(FeatureService); + +export function makeReqCtx( + orgId: string | undefined, +): PartialMessage { + return { + resourceOwner: orgId + ? { case: "orgId", value: orgId } + : { case: "instance", value: true }, + }; +} diff --git a/packages/zitadel-client2/src/v3alpha.ts b/packages/zitadel-client2/src/v3alpha.ts new file mode 100644 index 00000000000..4a547cdf99f --- /dev/null +++ b/packages/zitadel-client2/src/v3alpha.ts @@ -0,0 +1,8 @@ +import { createClientFor } from "./helpers"; +import { UserSchemaService } from "@zitadel/proto/zitadel/user/schema/v3alpha/user_schema_service_connect"; +import { UserService } from "@zitadel/proto/zitadel/user/v3alpha/user_service_connect"; +import { ActionService } from "@zitadel/proto/zitadel/action/v3alpha/action_service_connect"; + +export const createUserSchemaServiceClient = createClientFor(UserSchemaService); +export const createUserServiceClient = createClientFor(UserService); +export const createActionServiceClient = createClientFor(ActionService); diff --git a/packages/zitadel-client2/tsconfig.json b/packages/zitadel-client2/tsconfig.json new file mode 100644 index 00000000000..5f0ea69110c --- /dev/null +++ b/packages/zitadel-client2/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@zitadel/tsconfig/tsup.json", + "include": ["./src/**/*"], + "exclude": ["dist", "build", "node_modules"] +} diff --git a/packages/zitadel-client2/tsup.config.ts b/packages/zitadel-client2/tsup.config.ts new file mode 100644 index 00000000000..7dfce27c9e1 --- /dev/null +++ b/packages/zitadel-client2/tsup.config.ts @@ -0,0 +1,13 @@ +import { defineConfig, Options } from "tsup"; + +export default defineConfig((options: Options) => ({ + entry: ["src/index.ts", "src/v1.ts", "src/v2beta.ts", "src/v3alpha.ts"], + format: ["esm", "cjs"], + treeshake: false, + splitting: true, + dts: true, + minify: false, + clean: true, + sourcemap: true, + ...options, +})); diff --git a/packages/zitadel-client2/turbo.json b/packages/zitadel-client2/turbo.json new file mode 100644 index 00000000000..985798e5eb3 --- /dev/null +++ b/packages/zitadel-client2/turbo.json @@ -0,0 +1,15 @@ +{ + "extends": [ + "//" + ], + "pipeline": { + "build": { + "outputs": [ + "dist/**" + ], + "dependsOn": [ + "generate" + ] + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e627a05a8c2..51e52a690e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -236,6 +236,25 @@ importers: specifier: ^1.139.0 version: 1.146.0 + packages/zitadel-client2: + dependencies: + '@bufbuild/protobuf': + specifier: ^1.8.0 + version: 1.9.0 + '@connectrpc/connect': + specifier: ^1.4.0 + version: 1.4.0(@bufbuild/protobuf@1.9.0) + '@zitadel/proto': + specifier: workspace:* + version: link:../zitadel-proto + devDependencies: + '@zitadel/tsconfig': + specifier: workspace:* + version: link:../zitadel-tsconfig + eslint-config-zitadel: + specifier: workspace:* + version: link:../eslint-config-zitadel + packages/zitadel-next: dependencies: '@zitadel/react': @@ -653,6 +672,11 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==, tarball: https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz} engines: {node: '>=0.1.90'} + '@connectrpc/connect@1.4.0': + resolution: {integrity: sha512-vZeOkKaAjyV4+RH3+rJZIfDFJAfr+7fyYr6sLDKbYX3uuTVszhFe9/YKf5DNqrDb5cKdKVlYkGn6DTDqMitAnA==, tarball: https://registry.npmjs.org/@connectrpc/connect/-/connect-1.4.0.tgz} + peerDependencies: + '@bufbuild/protobuf': ^1.4.2 + '@cypress/request@3.0.1': resolution: {integrity: sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==, tarball: https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz} engines: {node: '>= 6'} @@ -5293,6 +5317,10 @@ snapshots: '@colors/colors@1.5.0': optional: true + '@connectrpc/connect@1.4.0(@bufbuild/protobuf@1.9.0)': + dependencies: + '@bufbuild/protobuf': 1.9.0 + '@cypress/request@3.0.1': dependencies: aws-sign2: 0.7.0