npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lafken/resolver

v0.16.1

Published

Lafken resolver foundation - AWS Lambda and IAM utilities for building decorator-based infrastructure resolvers

Readme

@lafken/resolver

@lafken/resolver is the foundation package for building custom resolvers within the Lafken framework. It provides the ResolverType interface, infrastructure primitives (LambdaHandler, Role), Lambda asset bundling (initLambdaAssetMetadata/lambdaAssets), and a global resource tracking system (lafkenResource) that enable developers to integrate any AWS service into Lafken.

If you want to create your own resolver — for example, to support a new AWS service or a custom integration — this package gives you everything you need.

How Resolvers Work

Lafken follows a Decorator → Resolver architecture:

  1. A decorator (built with @lafken/common) captures metadata about a class or method at build time.
  2. A resolver reads that metadata and generates the corresponding CDKTN infrastructure.

Every resolver implements the ResolverType interface and is registered in createApp(). The framework invokes resolver lifecycle hooks in order:

beforeCreate(scope)  →  create(module, resource)  →  afterCreate(scope)
  • beforeCreate — Called once per resolver before any resource is processed. Use it to create shared infrastructure (e.g., an API Gateway, an EventBridge bus).
  • create — Called once per decorated resource. This is where you read metadata and generate the resource's infrastructure.
  • afterCreate — Called once after all resources are processed. Use it to finalize configurations (e.g., wire integrations, build Lambda assets).

Getting Started

This example walks through creating a complete custom resolver for AWS SNS topics.

1. Define the Decorator

Use createResourceDecorator and createLambdaDecorator from @lafken/common to capture metadata:

// src/main/sns.ts
import { createResourceDecorator, createLambdaDecorator } from '@lafken/common';

export const RESOURCE_TYPE = 'SNS' as const;

export interface TopicProps {
  name?: string;
}

export interface PublishProps {
  name: string;
}

export interface PublishMetadata extends PublishProps {
  name: string;
}

// Class decorator — marks a class as an SNS resource
export const Topic = createResourceDecorator<TopicProps>({
  type: RESOURCE_TYPE,
  callerFileIndex: 5,
});

// Method decorator — marks a method as a handler
export const Publish = (props: PublishProps) =>
  createLambdaDecorator<PublishProps, PublishMetadata>({
    getLambdaMetadata: (props, methodName) => ({
      ...props,
      name: methodName,
    }),
  })(props);

2. Implement the Resolver

// src/resolver/resolver.ts
import {
  type ClassResource,
  getResourceMetadata,
  getResourceHandlerMetadata,
  type ResourceMetadata,
} from '@lafken/common';
import {
  type AppModule,
  getContextValueByScope,
  initLambdaAssetMetadata,
  LambdaHandler,
  type ResolverType,
} from '@lafken/resolver';
import { SnsTopic } from '@cdktn/provider-aws/lib/sns-topic';
import { SnsTopicSubscription } from '@cdktn/provider-aws/lib/sns-topic-subscription';
import { RESOURCE_TYPE, type PublishMetadata } from '../main/sns';

export class SnsResolver implements ResolverType {
  public type = RESOURCE_TYPE;

  public create(module: AppModule, resource: ClassResource) {
    const metadata: ResourceMetadata = getResourceMetadata(resource);
    const handlers = getResourceHandlerMetadata<PublishMetadata>(resource);
    const contextBundler = getContextValueByScope(module, 'bundler');

    // Register the Lambda source file so `lambdaAssets.createAssets()` bundles it later
    initLambdaAssetMetadata({ metadata, handlers, contextBundler });

    // Create SNS Topic
    const topic = new SnsTopic(module, `${metadata.name}-topic`, {
      name: metadata.name,
    });

    // Create a Lambda + subscription for each handler
    for (const handler of handlers) {
      const id = `${handler.name}-${metadata.name}`;

      const lambda = new LambdaHandler(module, id, {
        ...handler,
        filename: metadata.filename,
        foldername: metadata.foldername,
        originalName: metadata.originalName,
        principal: 'sns.amazonaws.com',
      });

      new SnsTopicSubscription(module, `${id}-subscription`, {
        topicArn: topic.arn,
        protocol: 'lambda',
        endpoint: lambda.arn,
      });
    }
  }
}

3. Register in Your App

import { createApp, createModule } from '@lafken/main';
import { SnsResolver } from './resolver/resolver';
import { NotificationService } from './modules/notifications';

const notifications = createModule({
  name: 'notifications',
  resources: [NotificationService],
});

createApp({
  name: 'my-app',
  modules: [notifications],
  resolvers: [new SnsResolver()],
});

4. Use the Decorators

// src/modules/notifications.ts
import { Topic, Publish } from '../main/sns';

@Topic({ name: 'order-events' })
export class NotificationService {
  @Publish({ name: 'order-created' })
  onOrderCreated() {
    // handler logic
  }

  @Publish({ name: 'order-shipped' })
  onOrderShipped() {
    // handler logic
  }
}

ResolverType Interface

The contract every resolver must implement:

interface ResolverType {
  type: string;
  beforeCreate?: (scope: AppStack) => Promise<void> | void;
  create: (module: AppModule, resource: ClassResource) => Promise<void> | void;
  afterCreate?: (scope: AppStack) => Promise<void> | void;
}

| Property | Type | Required | Description | |---|---|---|---| | type | string | Yes | Unique identifier that matches the type set by the resource decorator. | | beforeCreate | (scope: AppStack) => void | No | Called once before any resource is processed. Receives the root stack. | | create | (module: AppModule, resource: ClassResource) => void | Yes | Called for each decorated resource whose type matches this resolver. | | afterCreate | (scope: AppStack) => void | No | Called once after all resources have been processed. Receives the root stack. |

Lifecycle Parameters

  • AppStack — The root TerraformStack with an id property. Available in beforeCreate and afterCreate.
  • AppModule — A scoped Construct representing the module that contains the resource. Available in create.
  • ClassResource — The decorated class itself. Use getResourceMetadata() and getResourceHandlerMetadata() from @lafken/common to extract metadata.

LambdaHandler

LambdaHandler creates AWS Lambda functions with automatic IAM roles, environment variable management, and context-aware configuration. It extends LambdaFunction from CDKTN via lafkenResource.make(), so it supports global resource tracking.

import { LambdaHandler } from '@lafken/resolver';

new LambdaHandler(module, 'process-order', {
  name: 'processOrder',
  filename: 'order-handler',
  foldername: 'src/handlers',
  originalName: 'OrderService',
  principal: 'apigateway.amazonaws.com', // optional invoke permission
  lambda: {
    memory: 256,
    timeout: 30,
    runtime: 22,
    enableTrace: true,
    services: ['dynamodb', 's3'],
    env: { TABLE_NAME: 'orders' },
  },
});

Configuration Resolution

LambdaHandler resolves configuration values using a hierarchical precedence:

handler-level  >  module-level  >  app-level  >  default

This applies to runtime, timeout, memory, and env. Values set directly on the handler override module-level config, which in turn overrides app-level globals.

LambdaHandlerProps

| Property | Type | Required | Description | |---|---|---|---| | name | string | Yes | Method name used as the Lambda handler entry point. | | filename | string | Yes | Source file name (without extension) for bundling. | | foldername | string | Yes | Source directory path for bundling. | | originalName | string | Yes | Original class name, used for asset generation. | | suffix | string | No | Appended to the function name for uniqueness. | | description | string | No | Lambda function description. | | principal | string | No | AWS service principal for invoke permission (e.g., apigateway.amazonaws.com). | | sourceArn | string | No | Restricts the invoke permission to a specific source ARN (e.g., a specific S3 bucket or API Gateway resource). Only applies when principal is set. | | sourceAccount | string | No | Restricts the invoke permission to a specific source AWS account. Only applies when principal is set. | | lambda | LambdaProps | No | Lambda-specific configuration — see Lambda Configuration (lambda prop) below. |

Lambda Configuration (lambda prop)

LambdaProps (from @lafken/common) covers more than memory/timeout/runtime/services/env/enableTrace shown above. LambdaHandler wires each of these into real CDKTN resources:

| Property | Type | What LambdaHandler does with it | |---|---|---| | tags | Record<string, string> | Applied as resource tags on the function. | | vpcConfig | VpcConfig | Deploys the function inside a VPC (putVpcConfig) so it can reach private resources (RDS, ElastiCache, internal services). | | ephemeralStorage | number | Sets the /tmp size in MB (512–10240). | | reservedConcurrency | number | Caps concurrent executions (0 throttles the function entirely). | | architecture | 'x86_64' \| 'arm64' | Sets the instruction set architecture. | | alias | AliasConfig | Publishes a version and creates a LambdaAlias pointing to it; if provisionedExecutions > 0, also creates a LambdaProvisionedConcurrencyConfig on that alias. | | loggingConfig | LoggingConfig | Configures CloudWatch logging (putLoggingConfig); if retentionInDays is set, creates and links a CloudwatchLogGroup. | | layers | string[] | Layer ARNs to attach. Layers declared at app, module, and handler level are all merged together, not overridden. | | outputs | ResourceOutputType<LambdaOutputAttributes> | Exports function attributes (arn, invokeArn, qualifiedArn) as SSM parameters or Terraform outputs via ResourceOutput. | | functionName | string | Overrides the auto-generated function name. | | ref | string | Registers the function globally (register('lambda', ref)), retrievable elsewhere via lafkenResource.getResource('lambda', ref) or Refs.resourceValue('lambda::<ref>', attr). |

See LambdaProps in @lafken/common's source for the full JSDoc on each field.

Role

Role creates IAM roles with predefined permission sets for common AWS services. It extends IamRole via lafkenResource.make().

import { Role } from '@lafken/resolver';

// Simple: grant default permissions for listed services
new Role(scope, 'service-role', {
  name: 'order-processor-role',
  services: ['dynamodb', 's3', 'sqs'],
});

// Fine-grained: specify permissions and resources
new Role(scope, 'restricted-role', {
  name: 'read-only-role',
  services: [
    { type: 'dynamodb', permissions: ['Query', 'GetItem'], resources: ['arn:aws:dynamodb:*:*:table/orders'] },
    { type: 's3', permissions: ['GetObject'], resources: ['arn:aws:s3:::my-bucket/*'] },
  ],
});

Supported Services

Each service name maps to a default set of IAM actions:

| Service | IAM Prefix | Default Actions | |---|---|---| | dynamodb | dynamodb: | Query, Scan, GetItem, BatchGetItem, PutItem, DeleteItem, UpdateItem, ConditionCheckItem | | s3 | s3: | Full CRUD — GetObject, PutObject, DeleteObject, ListBucket, and more | | lambda | lambda: | InvokeFunction | | cloudwatch | logs: | CreateLogGroup, CreateLogStream, PutLogEvents, and more | | sqs | sqs: | SendMessage, ReceiveMessage, DeleteMessage, GetQueueUrl, GetQueueAttributes | | state_machine | states: | InvokeHTTPEndpoint, DescribeExecution, StartExecution, StopExecution, GetExecutionHistory | | kms | kms: | Encrypt, Decrypt, GenerateDataKey, DescribeKey, and more | | ssm | ssm: | GetParameter, GetParameters, PutParameter, DescribeParameters, and more | | event | events: | DescribeEventRule, DescribeEventBus, DescribeRule, PutEvents, PutRule | | kinesis | kinesis: | PutRecord, PutRecords |

RoleProps

| Property | Type | Required | Description | |---|---|---|---| | name | string | Yes | IAM role name. | | services | Services[] | Yes | Service permissions — an array of service names or fine-grained permission objects. | | principal | string | No | AWS service principal for AssumeRole. Defaults to lambda.amazonaws.com. |

Custom Service Permissions

For services not in the predefined list, use the custom type:

new Role(scope, 'custom-role', {
  name: 'ses-sender-role',
  services: [
    { type: 'custom', serviceName: 'ses', permissions: ['SendEmail', 'SendRawEmail'], resources: ['*'] },
  ],
});

lafkenResource

lafkenResource is the global resource registry. It provides two core capabilities:

  1. Mixin creationlafkenResource.make(BaseClass) enhances any CDKTN Construct with a register() method.
  2. Global tracking — Resources registered with register() can be retrieved from anywhere using getResource().

make(BaseClass)

Creates a subclass that adds resource tracking methods. BaseClass must extend CDKTN's Construct — this works for any Terraform resource construct, not just the ones Lafken already ships a resolver for:

import { lafkenResource } from '@lafken/resolver';
import { SnsTopic } from '@cdktn/provider-aws/lib/sns-topic';

// Create a trackable version of SnsTopic
class TrackableTopic extends lafkenResource.make(SnsTopic) {}

const topic = new TrackableTopic(scope, 'my-topic', { name: 'events' });

// Register globally so other resolvers can reference it
topic.register('notifications', 'events-topic');

register(namespace, id)

Registers a resource instance under a namespace::id key so other resources can look it up:

topic.register('notifications', 'events-topic');
// Retrievable as 'notifications::events-topic'

namespace accepts any of the built-in RegisterNamespaces values ('api', 'bucket', 'dynamo', 'queue', 'lambda', ...) or an arbitrary string, so custom/unsupported resources can pick their own namespace.

getResource(module, id)

Retrieves a globally registered resource:

const topic = lafkenResource.getResource<SnsTopic>('notifications', 'events-topic');
console.log(topic.arn);

Prefer Refs.resourceValue() (below) over calling getResource() directly for cross-resource values — it returns a deferred token that's safe to embed in config regardless of resource declaration order, whereas getResource() requires the target to already be registered at call time.

reset()

Clears the registry:

lafkenResource.reset();

lafkenResource is a module-level singleton, so this is mainly useful in tests — call it in beforeEach() to stop resources registered in one test case from leaking into the next.

Wrapping an arbitrary Terraform resource

Because make() accepts any CDKTN construct, it's the escape hatch for using a Terraform resource Lafken has no dedicated decorator/resolver for. Wrap it, register() it under a namespace of your choice, then read it from anywhere — including a Lambda's env — with Refs.resourceValue('namespace::id', attribute) (see Environment Variables):

// resolver.ts — any resolver's create()/beforeCreate()
import { lafkenResource } from '@lafken/resolver';
import { CloudfrontDistribution } from '@cdktn/provider-aws/lib/cloudfront-distribution';

class TrackableDistribution extends lafkenResource.make(CloudfrontDistribution) {}

const distribution = new TrackableDistribution(module, 'cdn', {
  /* ...distribution config... */
});

distribution.register('cdn', 'assets-distribution');
// elsewhere — a LambdaHandler's env, in the same or a different resolver
import { Refs } from '@lafken/common';

lambda: {
  env: {
    DISTRIBUTION_ID: Refs.resourceValue('cdn::assets-distribution', 'id'),
    DISTRIBUTION_DOMAIN: Refs.resourceValue('cdn::assets-distribution', 'domainName'),
  },
}

Refs.resourceValue returns a Lazy CDKTN token that looks the resource up in the registry at synth time, so declaration order between the wrapped resource and the Lambda that references it doesn't matter — as long as both exist by the time createApp() synthesizes the stack.

lambdaAssets

lambdaAssets manages the build and bundling pipeline for Lambda functions using Rolldown. It handles code splitting, minification, and asset packaging. Every built-in resolver (api, queue, event, schedule, state-machine, standalone, pubsub, ...) registers its Lambda source files through the initLambdaAssetMetadata() helper described below rather than calling lambdaAssets directly.

initLambdaAssetMetadata(props)

Exported from @lafken/resolver's utils. Registers metadata for a Lambda source file, merging the resource's own bundler config with the app/module-level one. Call it in the resolver's create method, before any LambdaHandler instances reference the same filename/foldername:

import { getContextValueByScope, initLambdaAssetMetadata } from '@lafken/resolver';

const contextBundler = getContextValueByScope(module, 'bundler');

initLambdaAssetMetadata({
  metadata,   // ResourceMetadata — provides filename, foldername, originalName, bundler
  handlers,   // LambdaMetadata[] — provides each handler's name
  contextBundler,
  streamingByMethod: { onOrderCreated: true }, // optional, per-method response streaming
  afterBuild: (outputPath) => { /* optional post-build hook */ },
});

| Property | Type | Required | Description | |---|---|---|---| | metadata | ResourceMetadata | Yes | The decorated resource's metadata (getResourceMetadata()); provides filename, foldername, originalName, and its own bundler config. | | handlers | LambdaMetadata[] | Yes | The decorated handlers (getResourceHandlerMetadata()); each contributes its name as an exported method. | | contextBundler | BundlerConfig | No | App/module-level bundler config (typically getContextValueByScope(module, 'bundler')), used as a fallback when the resource sets no bundler.minify. | | streamingByMethod | Record<string, boolean> | No | Marks specific handler methods for response streaming (wrapped with awslambda.streamifyResponse at build time). | | afterBuild | (outputPath: string) => void | No | Called after the asset is bundled, with the output directory path. |

lambdaAssets.initializeMetadata()/addLambda() are the lower-level methods initLambdaAssetMetadata() and LambdaHandler call internally — resolvers should use initLambdaAssetMetadata() instead of calling them directly.

createAssets()

Builds all registered Lambda assets. Called automatically by the framework after all resolvers complete. Each asset is bundled with Rolldown as a CJS module targeting Node.js, with @aws-sdk, aws-lambda, and node:* as externals (plus any externalPackages set on the app/module/resource bundler config).

Environment Variables

Lambda environment variables (EnvironmentValue = Record<string, string>) are passed straight through to the underlying aws_lambda_function resource — there is no dedicated Environment construct or SSM-string convention to parse. Static values are plain strings; dynamic values are produced by calling Refs.resourceValue/Refs.ssmValue (from @lafken/common) directly, since both already return a real, deferred CDKTN token by the time they land in the config:

import { Refs } from '@lafken/common';

lambda: {
  env: {
    TABLE_NAME: 'orders',
    TABLE_ARN: Refs.resourceValue('database::orders-table', 'arn'),
    API_KEY: Refs.ssmValue('/config/api-key'),
    DB_PASSWORD: Refs.ssmValue('/config/db-password', true), // secure string
  },
}

These functions are resolved lazily by CDKTN at synth time, regardless of the declaration order between the resources involved — see @lafken/common's Cross-Resource Refss for the full list of available reference functions (Refs.resourceValue, Refs.ssmValue, Refs.accountId, Refs.callerArn, Refs.region, Refs.partition, Refs.dnsSuffix, fn, token) and how registerRefResolvers wires this resolver package's implementation into them.

Context Utilities

createApp()/createModule() store their globalConfig.lambda under CDKTN construct context (app/module context names). These helpers read it back, which is how LambdaHandler's Configuration Resolution hierarchy (handler > module > app > default) is implemented — use them in a custom resolver to honor the same hierarchy for its own config.

import { getAppContext, getModuleContext, getContextValueByScope } from '@lafken/resolver';

// inside a resolver's create(module, resource)
const appContext = getAppContext(module); // GlobalContext set on createApp()
const moduleContext = getModuleContext(module); // GlobalContext set on createModule(), if any

// Or read a single key, falling back from module- to app-level automatically:
const contextBundler = getContextValueByScope(module, 'bundler');

| Function | Description | |---|---| | getAppContext(scope) | Returns the app-level GlobalContext. | | getModuleContext(scope) | Returns the module-level GlobalContext, if the module set one. | | getContextValue(key, appContext, moduleContext) | Returns moduleContext[key] ?? appContext[key]. | | getContextValueByScope(scope, key) | Combines the three calls above — resolves appContext/moduleContext from scope and returns the module-over-app value for key. |

Testing Utilities

setupTestingStack()

Creates a test-ready CDKTN stack for unit testing resolvers:

import { setupTestingStack } from '@lafken/resolver';

const { app, stack } = setupTestingStack();

setupTestingStackWithModule()

Creates a stack with a pre-configured module scope:

import { setupTestingStackWithModule } from '@lafken/resolver';

const { app, stack, module } = setupTestingStackWithModule();

enableBuildEnvVariable()

Decorators only capture metadata during builds. In tests, enable build mode first:

import { enableBuildEnvVariable } from '@lafken/common';

describe('SnsResolver', () => {
  enableBuildEnvVariable();

  // Now decorators will work
  @Topic({ name: 'test-topic' })
  class TestResource {
    @Publish({ name: 'test' })
    handler() {}
  }
});

Building a Custom Resolver — Summary

  1. Create decorators with createResourceDecorator() / createLambdaDecorator() from @lafken/common. Set a unique type string.
  2. Implement ResolverType — set type to match your decorator, implement create() to process resources, optionally use beforeCreate() / afterCreate() for shared or deferred setup.
  3. Use LambdaHandler to create Lambda functions with automatic IAM, context, and environment management.
  4. Use initLambdaAssetMetadata() to register Lambda source files; lambdaAssets.createAssets() bundles them automatically after all resolvers complete.
  5. Use lafkenResource.make() to extend any CDKTN construct with global resource tracking.
  6. Register your resolver in createApp({ resolvers: [new YourResolver()] }).