Agent onboarding guide for the @project-kessel/kessel-sdk Node.js SDK. This file is the entry point for any AI agent (Claude, Cursor, CodeRabbit, etc.) working in this repository.
Directory-local GUIDELINES.md files contain detailed, domain-specific rules. Read the relevant file before making changes in that area.
src/kessel/auth/GUIDELINES.md-- OAuth2 credentials, OIDC discovery, token caching, AuthRequestsrc/kessel/inventory/GUIDELINES.md-- ClientBuilder, per-version wiring, generated vs hand-written codesrc/kessel/rbac/GUIDELINES.md-- REST workspace helpers, resource/subject factories, paginated streamingsrc/kessel/console/GUIDELINES.md-- Red Hat identity header parsing, principal extractionexamples/GUIDELINES.md-- Runnable integration examples, file naming, script registration
A TypeScript gRPC SDK for Project Kessel. The SDK provides:
- Generated gRPC client stubs for the Kessel Inventory API
- A fluent
ClientBuilderpattern for client construction - OAuth 2.0 Client Credentials authentication (via optional
oauth4webapidependency) - RBAC workspace REST helpers
- Console identity header parsing and principal extraction
- A
promisifyClientutility that converts callback-based gRPC clients to Promise-based
Published as @project-kessel/kessel-sdk on npm. Requires Node >= 20.
src/
kessel/
auth/ # Hand-written. OAuth2 client credentials, OIDC discovery
__tests__/
console/ # Hand-written. Red Hat identity header parsing
__tests__/
grpc/ # Hand-written. gRPC call credentials helper
__tests__/
inventory/
index.ts # Hand-written. ClientBuilder base class, clientBuilderForStub()
v1/ # Generated (health.ts) + hand-written (index.ts)
v1beta1/ # Generated + hand-written (index.ts). Legacy, NOT in package exports
v1beta2/ # Generated + hand-written (index.ts). Current primary API
__tests__/
rbac/
v2.ts # Hand-written. REST workspace helpers, resource/subject factories
__tests__/
google/ # Generated. google.protobuf and google.api types
buf/validate/ # Generated. buf validation types
promisify.ts # Hand-written. Proxy-based gRPC client promisification
examples/ # Integration examples (NOT automated tests). Require live server
builder/ # Examples using ClientBuilder + buildAsync()
vanilla/ # Examples using raw gRPC stubs + callbacks
rbac/ # Examples for RBAC workspace operations
console/ # Examples for console identity helpers
dist/ # Build output (gitignored contents, committed structure)
cjs/ # CommonJS build
esm/ # ES module build
types/ # TypeScript declarations
tsc-alias-replacers/ # ESM import path fixer for protobufjs/minimal
This is the most important architectural distinction in the repo.
Generated code (never hand-edit):
- All
.tsfiles insrc/kessel/inventory/v1/,v1beta1/,v1beta2/EXCEPTindex.ts - All files in
src/google/andsrc/buf/ - Identified by the header:
// Code generated by protoc-gen-ts_proto. DO NOT EDIT. - Contains
/* eslint-disable */at the top - Regenerated automatically every 6 hours via
.github/workflows/buf-generate.ymlfrom upstream protos atbuf.build/project-kessel/inventory-api - To regenerate manually:
buf generate && npm run prettier
ts-proto generation options (in buf.gen.yaml):
outputServices=grpc-js-- generates@grpc/grpc-js-compatible client/server stubsoutputServices=generic-definitions-- emits*Definitionobjects for generic service metadatauseOptionals=all-- every protobuf field becomesfield?: Type | undefinedesModuleInterop=true,env=node,outputExtensions=true- Do not change these options without verifying that
ClientBuilder,promisifyClient, and all examples still compile
Generated message shape: Every protobuf message type exports a TypeScript interface and a companion object with encode, decode, fromJSON, toJSON, create, and fromPartial methods, plus DeepPartial<T> and Exact<P, I> helper types (repeated per file -- this is normal for ts-proto). Generated enums use numeric values with an UNRECOGNIZED = -1 sentinel.
Hand-written code (where your changes go):
src/kessel/inventory/index.ts--ClientBuilderabstract class andclientBuilderForStub()factorysrc/kessel/inventory/*/index.ts-- Per-version wiring (imports generated client, exportsClientBuilder)src/kessel/auth/index.ts-- OAuth2 client credentialssrc/kessel/console/index.ts-- Red Hat identity header parsingsrc/kessel/grpc/index.ts-- gRPC call credentials helpersrc/kessel/rbac/v2.ts-- RBAC REST helperssrc/promisify.ts-- Client promisification utility- All
__tests__/files
npm run build-- Runs CJS, ESM, and type declaration builds in parallel (npm-run-all)npm run build:cjs-- TypeScript to CommonJS, injects{"type":"commonjs"}intodist/cjs/package.jsonnpm run build:esm-- TypeScript to ESM, runstsc-aliasto fix import paths, injects{"type":"module"}intodist/esm/package.jsonnpm run build:types-- Declaration-only emit todist/types/npm test-- Jest with ts-jestnpm run lint-- ESLint with auto-fix (coverssrcandexamples)npm run lint:check-- ESLint without auto-fix (used in CI)npm run prettier-- Prettier with auto-fixnpm run prettier:check-- Prettier without auto-fix (used in CI)
CI (.github/workflows/build.yml) runs on every push/PR to main, testing Node 20, 22, and 24. All four checks must pass: lint, prettier, build, test.
Dependabot (.github/dependabot.yml) checks both npm and GitHub Actions dependencies daily. Review and merge security updates promptly.
- Generated protobuf files use
snake_case.tsmatching the proto file name (e.g.,check_request.ts,resource_reference.ts,inventory_service.ts) - Hand-written module entry points are always
index.ts - Test files inside
__tests__/match the source file name (e.g.,__tests__/index.tstests../index.ts,__tests__/v2.tstests../v2.ts) - No
.test.tssuffix is used when tests are in__tests__/directories
camelCasefor functions, methods, variables, and properties (e.g.,buildAsync,oauth2CallCredentials,fetchOIDCDiscovery)- Exported factory functions use
camelCaseverbs (e.g.,clientBuilderForStub,principalSubject,workspaceResource) - Constants use
UPPER_SNAKE_CASE(e.g.,EXPIRATION_WINDOW_MILLI,DEFAULT_PAGE_LIMIT,WORKSPACE_ENDPOINT)
PascalCasefor interfaces, classes, types, and enums (e.g.,ClientBuilder,OAuth2ClientCredentials,CheckRequest,SubjectReference)- Generated service clients follow
Kessel*ServiceClientpattern (e.g.,KesselInventoryServiceClient,KesselInventoryHealthServiceClient) - Generated enums use
UPPER_SNAKE_CASEvalues with anUNRECOGNIZED = -1sentinel
- Per-version
index.tsfiles export a singleClientBuilderconst (v1, v1beta2) or multiple named builders (v1beta1) - The
ClientBuilderexport is the result of callingclientBuilderForStub(GeneratedServiceClient)
- Target: ES2020. Module: ES2022. Module resolution:
node strict: truebutstrictNullChecks: false-- protobuf fields are all optional (field?: Type | undefined), so null checks are disabled project-wide- Unused variables/args prefixed with
_are allowed (eslint ruleargsIgnorePattern: "^_") @typescript-eslint/no-explicit-anyiserrorin source,offin test files- Prettier and ESLint are both enforced; run
npm run prettier && npm run lintbefore committing
- Source code uses relative imports (e.g.,
import { OAuth2ClientCredentials } from "../auth") - Examples and consumers use package subpath imports (e.g.,
import { ClientBuilder } from "@project-kessel/kessel-sdk/kessel/inventory/v1beta2") - Tests use relative imports from source, never package imports
- There is no root/barrel export -- consumers must import from specific subpaths
- Builder pattern with fluent API for client construction (method chaining returns
this) - Factory function pattern:
clientBuilderForStub()creates concrete builder classes from generated stubs - Proxy-based promisification:
promisifyClient()usesProxyto lazily wrap callback methods - Optional dependency via dynamic
import():oauth4webapiis loaded only when auth is needed Object.freeze()on returned token response objects (immutable by convention)- Async generator pattern for paginated streaming (see
listWorkspacesinrbac/v2.ts)
These conventions apply across all modules. See individual GUIDELINES.md files for module-specific error details.
- No custom error classes. The SDK uses plain
Errorand@grpc/grpc-jsServiceError. Do not define custom error types. - Let errors propagate. Do not catch errors inside SDK source to suppress them. Let them reach the caller.
- Three distinct code spaces. Do not conflate gRPC
ServiceError.code(gRPC status enum, e.g. 5 = NOT_FOUND),google.rpc.Status.code(in bulk response per-item errors), and HTTP status codes (in RBAC fetch responses). They are separate systems. - Bulk per-item errors.
checkBulk,checkSelfBulk, andcheckForUpdateBulkuse a per-item error model. A successful RPC does not mean every item succeeded -- always checkpair.erroron eachCheckBulkResponsePair.pair.itemandpair.errorare mutually exclusive. ALLOWED_FALSEis not an error. It is a valid authorization response.ALLOWED_UNSPECIFIED(0) typically means the field was not set and should be handled defensively.
Every check and list request accepts an optional consistency field that trades latency for freshness:
| Mode | When to use | Latency |
|---|---|---|
minimizeLatency: true |
Read-heavy dashboards, UI filtering | Lowest |
atLeastAsFresh: { token } |
Read-after-write with a known token | Medium |
atLeastAsAcknowledged: true |
Must see all acknowledged writes | Higher |
| Omitted | Server default (typically minimizeLatency) |
Varies |
For reportResource, writeVisibility controls the write side: MINIMIZE_LATENCY returns quickly (subsequent checks may not reflect the write), while IMMEDIATE waits for acknowledgment. For security-critical checks immediately after permission changes, use atLeastAsAcknowledged or atLeastAsFresh with a token from the write response.
Tests use Jest with ts-jest. See individual GUIDELINES.md files for module-specific test patterns.
toBefor primitives and identity checks;toEqualfor deep object comparisonrejects.toThrow("message")for async error assertions;expect(() => ...).toThrow("message")for sync- Verify mock call counts explicitly:
expect(mock).toHaveBeenCalledTimes(n) - Use
toBeFalsy()(nottoBe(false)) for checks that may returnundefinedorfalse
- Generated protobuf types (auto-generated by
buf generate) - The
promisify.tsutility internals (tested indirectly through ClientBuilder tests) - The
dist/output (tested through the CI build step)
The project uses conventional-ish commits but does not enforce them with tooling:
chore: bump version to X.Y.Zfor version bumps- Descriptive imperative titles for feature/fix PRs (e.g., "Add example for running a check_bulk request")
Regenerate protobuf files from buffor automated protobuf PRs- Dependabot PRs use the default
Bump <package> from X to Yformat
- All CI checks must pass: lint, prettier, build, test (Node 20/22/24)
- Do not edit generated files -- changes to protobuf types must go through the upstream proto definitions at
buf.build/project-kessel/inventory-api - Review automated buf-generate PRs for unexpected changes to security-sensitive types (
CheckRequest,Consistency,SubjectReference, etc.) - New public API surfaces (new version directories, new exported modules) require:
- A hand-written
index.tswithClientBuilderwiring - Corresponding entries in
package.jsonexports(require/import/types triple + wildcard) - Tests in a colocated
__tests__/directory
- A hand-written
- Keep the runtime dependency count minimal (currently:
@grpc/grpc-jsrequired,oauth4webapioptional)
See examples/GUIDELINES.md for full conventions on file naming, script structure, imports, environment variables, and npm script registration.
When public API surface is added or changed, update the examples/ directory so that examples stay accurate and demonstrate current usage. Add examples for new API operations, new subpath exports, and changed method signatures. Remove examples for removed API surface.
-
Editing generated files: Files with
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.are overwritten every 6 hours by CI. Your changes will be lost. Fix issues in the upstream.protodefinitions instead. -
Forgetting package.json exports: Adding a new module under
src/kessel/without adding the correspondingexportsentry inpackage.jsonmeans consumers cannot import it. Each public subpath needsrequire,import, andtypesconditions, plus a wildcard entry for deep imports. -
Importing from package root: There is no root export (
@project-kessel/kessel-sdk). Always import from a specific subpath. -
Mixing auth with insecure channels: The builder throws
"Invalid credential configuration: can not authenticate with insecure channel"if you combine call credentials withinsecure(). This is validated at configuration time, not at call time. -
Creating multiple client instances: Each
build()/buildAsync()call creates a new gRPC client with its own HTTP/2 channel. Create one client and reuse it. -
Assuming streaming methods are promisified:
buildAsync()only promisifies unary methods. Streaming methods (streamedListObjects,streamedListSubjects) remain as-is because they already returnAsyncIterable. -
Testing with package imports instead of relative imports: Tests must use relative imports from source (
../index), not package subpath imports (@project-kessel/kessel-sdk/...). Package imports are only used inexamples/. -
Forgetting
strictNullChecksis off: The tsconfig hasstrictNullChecks: false. Code that works here may fail in consumer projects with stricter settings. Be aware when writing type signatures. -
Treating a successful bulk RPC as fully successful: Bulk endpoints use per-item errors. Always iterate
response.pairsand checkpair.erroron each entry. See the Error Handling Conventions section. -
Not closing gRPC clients:
@grpc/grpc-jsclients hold open HTTP/2 connections. Callclient.close()during graceful shutdown. Failing to close clients in short-lived processes (CLI tools, serverless functions) can cause the Node.js process to hang on exit. -
Needing gRPC channel options with ClientBuilder:
ClientBuilderdoes not expose@grpc/grpc-jschannel options (keepalive, message size). If you need these, use the vanilla constructor withClientOptionsas the third argument.
Consumer code
|
|-- imports from package subpaths (e.g., kessel/inventory/v1beta2)
|
v
index.ts (hand-written, per version)
|-- calls clientBuilderForStub(GeneratedServiceClient) from inventory/index.ts
|
v
ClientBuilder (abstract, in inventory/index.ts)
|-- .build() --> raw gRPC client (generated stub)
|-- .buildAsync() --> Proxy-wrapped client (promisify.ts)
|-- .oauth2ClientAuthenticated() --> uses oauth2CallCredentials (grpc/index.ts)
| |
| v
| OAuth2ClientCredentials (auth/index.ts)
| |-- dynamic import("oauth4webapi")
| |-- token caching + refresh
|
v
Generated gRPC stubs (protoc-gen-ts_proto output)
|-- use @grpc/grpc-js for transport
|-- use protobufjs/minimal for serialization
Console module (console/index.ts)
|-- parses x-rh-identity header
|-- extracts principal via principalSubject (rbac/v2.ts)
RBAC module (rbac/v2.ts) -- separate from the gRPC path
|-- uses native fetch() for REST calls
|-- uses AuthRequest interface for HTTP auth
|-- uses generated protobuf types for request/response shapes
|-- provides async generator for paginated streaming via gRPC
The key insight is that inventory/index.ts is the bridge layer: it provides the ClientBuilder base class and clientBuilderForStub() factory that connect hand-written auth/config logic to generated gRPC stubs. Each API version's index.ts simply calls this factory with its specific generated client class.