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

declastruct-aws

v1.13.0

Published

declarative control of Aws constructs via declastruct

Readme

declastruct-aws

test publish

Declarative control of Aws resource constructs, via declastruct.

Declare the structures you want. Plan to see the changes required. Apply to make it so 🪄

install

npm install -s declastruct-aws

use via cli

example.1

wish ✨

declare the resources you wish to have - and what state you wish them to be in

import { UnexpectedCodePathError } from 'helpful-errors';

export const getProviders = async (): Promise<DeclastructProvider[]> => [
  getDeclastructAwsProvider(
    {},
    {
      log: console,
    },
  ),
];

export const getResources = async (): Promise<DomainEntity<any>[]> => {
  // declare the resources you wish to construct
};

plan 🔮

plan how to achieve the wish of resources you've declared

this will emit a plan that declares the changes required in order to fulfill the wish

npx declastruct plan --wish provision/github/resources.ts --output provision/github/.temp/plan.json

apply 🪄

apply the plan to fulfill the wish

this will apply only the changes declared in the plan - and only if this plan is still applicable

npx declastruct apply --plan provision/github/.temp/plan.json

example.2 = open a vpc tunnel via an ec2 instance

import { RefByUnique } from 'domain-objects';
import { getDeclastructAwsProvider, DeclaredAwsRdsCluster, DeclaredAwsEc2Instance, DeclaredAwsSsmVpcTunnel } from 'declastruct-aws';

export const getProviders = async (): Promise<DeclastructProvider[]> => [
  getDeclastructAwsProvider(
    {},
    {
      log: console,
    },
  ),
];

export const getResources = async (): Promise<DomainEntity<any>[]> => {
  const cluster = RefByUnique.as<typeof DeclaredAwsRdsCluster>({
    name: 'yourdb',
  });
  const bastion = RefByUnique.as<typeof DeclaredAwsEc2Instance>({
    exid: 'vpc-main-bastion',
  })
  const tunnel = DeclaredAwsSsmVpcTunnel.as({
    via: { mechanism: 'aws.ssm', bastion }
    into: { cluster },
    from: { host: 'localhost', port: 777_5432 },
    status: "OPEN",
  })
  return [tunnel];
};

example.3 = open an ssh tunnel to an ec2 instance

import * as fs from 'fs';
import { RefByUnique } from 'domain-objects';
import {
  getDeclastructAwsProvider,
  DeclaredAwsEc2Instance,
  DeclaredAwsEc2SshKeyAuthorized,
  DeclaredAwsSsmSshTunnel,
} from 'declastruct-aws';

export const getProviders = async (): Promise<DeclastructProvider[]> => [
  getDeclastructAwsProvider(
    {},
    {
      log: console,
    },
  ),
];

export const getResources = async (): Promise<DomainEntity<any>[]> => {
  const instance = RefByUnique.as<typeof DeclaredAwsEc2Instance>({
    exid: 'my-dev-instance',
  });

  // authorize your SSH key on the instance
  const sshKey = DeclaredAwsEc2SshKeyAuthorized.as({
    instance,
    publicKey: fs.readFileSync(`${process.env.HOME}/.ssh/id_ed25519.pub`, 'utf8'),
    comment: 'my-laptop',
  });

  // open SSH tunnel via SSM
  const sshTunnel = DeclaredAwsSsmSshTunnel.as({
    instance,
    from: { port: 2222 },
    into: { port: 22 },
    status: 'OPEN',
  });

  return [sshKey, sshTunnel];
};

after npx declastruct apply, SSH in:

ssh -i ~/.ssh/id_ed25519 -p 2222 ec2-user@localhost

example.4 = provision an ec2 instance with hibernation

import { RefByUnique } from 'domain-objects';
import {
  getDeclastructAwsProvider,
  DeclaredAwsEc2LaunchTemplate,
  DeclaredAwsEc2Instance,
  DeclaredAwsEc2InstanceSession,
  DeclaredAwsVpcSubnet,
  DeclaredAwsVpcSecurityGroup,
} from 'declastruct-aws';

export const getProviders = async (): Promise<DeclastructProvider[]> => [
  getDeclastructAwsProvider(
    {},
    {
      log: console,
    },
  ),
];

export const getResources = async (): Promise<DomainEntity<any>[]> => {
  // declare launch template with hibernation enabled
  const template = DeclaredAwsEc2LaunchTemplate.as({
    exid: 'my-dev-template',
    instanceType: 't3.micro',
    imageId: 'ami-0c55b159cbfafe1f0',  // Amazon Linux 2023
    hibernation: true,
    rootVolumeEncrypted: true,  // required for hibernation
    rootVolumeSize: 16,         // must be >= instance RAM
    iamInstanceProfile: null,
    userData: null,
    metadataOptions: null,  // null = secure default (imdsv2-only: required / hop 1 / enabled)
    // docker/container box that needs the extra hop? add the export to the import above:
    //   import { ec2InstanceMetadataOptionsSecure } from 'declastruct-aws';
    // then override just the hop limit off it (stays imdsv2-only):
    //   metadataOptions: { ...ec2InstanceMetadataOptionsSecure, httpPutResponseHopLimit: 2 }
    tags: { purpose: 'dev' },
  });

  // declare instance
  const instance = DeclaredAwsEc2Instance.as({
    exid: 'my-dev-instance',
    template: { exid: template.exid },
    subnet: RefByUnique.as<typeof DeclaredAwsVpcSubnet>({ exid: 'my-subnet' }),
    securityGroups: [RefByUnique.as<typeof DeclaredAwsVpcSecurityGroup>({ exid: 'my-sg' })],
    tags: { purpose: 'dev' },
  });

  // control lifecycle via session
  const session = DeclaredAwsEc2InstanceSession.as({
    instance: { exid: instance.exid },
    status: 'active',  // 'active' | 'stopped' | 'hibernated'
  });

  return [template, instance, session];
};

to hibernate the instance, change status: 'hibernated' and re-apply:

npx declastruct plan --wish resources.ts --into plan.json
npx declastruct apply --plan plan.json

upgrade note — the secure metadata default plans a change on prior boxes. an already-deployed launch template that predates this secure default reads back as imdsv1-allowed, so declastruct plan will show a change against it (the default is imdsv2-only: required / hop 1 / enabled). a launch template is immutable, so apply does NOT heal it in place — the set fails loud. to adopt the secure posture, prune the old template + its instances and re-apply to create them imdsv2-only. to keep the old posture, opt out on the declaration: metadataOptions: { ...ec2InstanceMetadataOptionsSecure, httpTokens: 'optional' }.

example.5 = deploy a lambda with version and alias

import { RefByUnique } from 'domain-objects';
import { ConstraintError } from 'helpful-errors';
import {
  calcAwsLambdaConfigHash,
  DeclaredAwsIamRole,
  DeclaredAwsIamRolePolicyAttachedInline,
  DeclaredAwsLambda,
  DeclaredAwsLambdaAlias,
  DeclaredAwsLambdaVersion,
  genDeclaredAwsLambdaCode,
  getDeclastructAwsProvider,
} from 'declastruct-aws';

export const getProviders = async (): Promise<DeclastructProvider[]> => [
  getDeclastructAwsProvider(
    {},
    {
      log: console,
    },
  ),
];

export const getResources = async (): Promise<DomainEntity<any>[]> => {
  // declare iam role for lambda execution
  const lambdaRole = DeclaredAwsIamRole.as({
    name: 'my-lambda-role',
    path: '/',
    description: 'Role for lambda execution',
    policies: [
      {
        effect: 'Allow',
        principal: { service: 'lambda.amazonaws.com' },
        action: 'sts:AssumeRole',
      },
    ],
    tags: { managedBy: 'declastruct' },
  });

  // declare inline policy for CloudWatch Logs permissions
  const lambdaRolePolicy = DeclaredAwsIamRolePolicyAttachedInline.as({
    name: 'cloudwatch-logs',
    role: RefByUnique.as<typeof DeclaredAwsIamRole>(lambdaRole),
    document: {
      statements: [
        {
          effect: 'Allow',
          action: [
            'logs:CreateLogGroup',
            'logs:CreateLogStream',
            'logs:PutLogEvents',
          ],
          resource: '*',
        },
      ],
    },
  });

  // declare lambda function ($LATEST) with code from zip
  const lambda = DeclaredAwsLambda.as({
    name: 'svc-sea-turtle.prod.getSandbars',
    runtime: 'nodejs18.x',
    handler: 'index.handler',
    timeout: 30,
    memory: 128,
    role: RefByUnique.as<typeof DeclaredAwsIamRole>(lambdaRole),
    envars: { NODE_ENV: 'production' },
    code: genDeclaredAwsLambdaCode({ zipUri: '.artifact/contents.zip' }),
    tags: { managedBy: 'declastruct' },
  });

  // publish immutable version (fingerprinted by code + config hash)
  const lambdaVersion = DeclaredAwsLambdaVersion.as({
    lambda: RefByUnique.as<typeof DeclaredAwsLambda>(lambda),
    hash: {
      code: lambda.code?.hash ?? ConstraintError.throw('lambda.code.hash is required'),
      config: calcAwsLambdaConfigHash({ of: lambda }),
    },
  });

  // point LIVE alias to this version
  const lambdaAlias = DeclaredAwsLambdaAlias.as({
    name: 'LIVE',
    lambda: RefByUnique.as<typeof DeclaredAwsLambda>(lambda),
    version: RefByUnique.as<typeof DeclaredAwsLambdaVersion>(lambdaVersion),
    description: 'live traffic alias',
  });

  return [lambdaRole, lambdaRolePolicy, lambda, lambdaVersion, lambdaAlias];
};

this pattern enables:

  • change detection: code.hash enables declastruct to detect when code changed and deploy only when needed
  • immutable versions: each deploy publishes a new version fingerprinted by hash: { code, config }
  • aliased endpoints: invoke via function:LIVE for stable endpoints across deploys
  • safe rollbacks: retarget aliases to previous versions without redeploy
  • canary deploys: use routingConfig.additionalVersionWeights to split traffic between versions

example.6 = manage SSM parameters — plaintext config + write-only secrets

declare non-secret config and secrets side by side. the secret is write-only: plan never reads its value (no GetParameter, no kms:Decrypt), so a least-privilege plan role needs only ssm:DescribeParameters for it — exactly the posture terraform cannot offer.

import {
  getDeclastructAwsProvider,
  DeclaredAwsSsmParameterPlain,
  DeclaredAwsSsmParameterSecure,
} from 'declastruct-aws';

export const getProviders = async () => [
  await getDeclastructAwsProvider({}, { log: console }),
];

export const getResources = async () => {
  // plaintext config — the value is NOT sensitive, so drift is detected by a
  // normal value-compare (plan reads it via GetParameter; no decrypt needed)
  const logLevel = DeclaredAwsSsmParameterPlain.as({
    name: '/svc-notifications/prod/log-level',
    value: 'info',
    description: 'the log level',
    tags: { managedBy: 'declastruct' },
  });

  // secret (SecureString) — WRITE-ONLY. plan never reads the value; supply a value to
  // create/rotate, leave it undefined to KEEP the extant secret (no read, no decrypt).
  // best practice: source the value from an env var set ONLY when you intend to write.
  const authToken = DeclaredAwsSsmParameterSecure.as({
    name: '/svc-notifications/prod/twilio/auth-token',
    value: process.env.TWILIO_AUTH_TOKEN, // undefined = keep; present = create/rotate
    keyId: null, // null = the account default aws/ssm key (a CMK is optional)
    description: 'twilio auth token',
    tags: { managedBy: 'declastruct' },
  });

  return [logLevel, authToken];
};
# a least-privilege plan role needs NO GetParameter and NO kms:Decrypt for the secret
npx declastruct plan  --wish resources.ts --into plan.json
npx declastruct apply --plan plan.json

this pattern enables:

  • write-only secrets: plan reconciles a SecureString via metadata only — no GetParameter, no kms:Decrypt, and no secret-derived artifact stored anywhere to leak
  • least-privilege plan roles: the plan role can be denied kms:Decrypt outright, so a CI plan job can no longer read prod secrets (the whole risk terraform bakes in)
  • explicit rotation: supply a value to write/rotate; leave it undefined for a steady-state KEEP — the secret is never read back into a plan or state file
  • plaintext value-compare: non-secret String params still detect value drift normally, since their value is not sensitive

example.7 = read cost reports — daily spend trend (1d / 1w / 1m) vs forecast

cost reports are read-only — you don't apply them, you read them. each is a saved Cost Explorer query as code: declare the range + how to slice, then read the resolved numbers as a typed domain object. read via the report DAO's get.one.byUnique.

import { asIsoTimeStamp } from 'iso-time';
import {
  getDeclastructAwsProvider,
  DeclaredAwsCostReportSpendObservedDao,
  DeclaredAwsCostReportSpendForecastDao,
} from 'declastruct-aws';

// source the aws context the reports read through
const provider = await getDeclastructAwsProvider({}, { log: console });
const { context } = provider;

// a lookback window that ends at the last full day (UTC), N days back
const asLookback = (input: { days: number }) => {
  const untilMs = Date.UTC(
    new Date().getUTCFullYear(),
    new Date().getUTCMonth(),
    new Date().getUTCDate(),
  ); // 00:00 today = end of the last full day (exclusive)
  const sinceMs = untilMs - input.days * 24 * 60 * 60 * 1000;
  return {
    since: asIsoTimeStamp(new Date(sinceMs).toISOString()),
    until: asIsoTimeStamp(new Date(untilMs).toISOString()),
  };
};

// daily spend trend, grouped by service, for 1d / 1w / 1m lookbacks
for (const [label, days] of [['1d', 1], ['1w', 7], ['1m', 30]] as const) {
  const report = await DeclaredAwsCostReportSpendObservedDao.get.one.byUnique(
    {
      range: asLookback({ days }),
      granularity: 'DAILY',
      groupBy: { dimension: 'SERVICE' },
      filter: null,
      metric: 'UnblendedCost',
    },
    context,
  );
  // report.total = spend over the window; report.buckets = the daily trend
  console.log(label, report?.total, report?.buckets?.length, 'day-buckets');
}

// forecast the month ahead, with an 80% confidence band
const forecast = await DeclaredAwsCostReportSpendForecastDao.get.one.byUnique(
  {
    range: {
      since: asIsoTimeStamp(new Date().toISOString()),
      until: asLookback({ days: -30 }).until, // ~30 days ahead
    },
    granularity: 'MONTHLY',
    metric: 'UnblendedCost',
    filter: null,
    predictionInterval: 80,
  },
  context,
);
// forecast.total = mean projection; forecast.points[].lower/upper = the confidence band
console.log('forecast', forecast?.total, forecast?.points);

this pattern enables:

  • spend trend: granularity: 'DAILY' returns one bucket per day (the trend); buckets[].groups is the per-service composition within each day
  • lookback windows: vary range to compare 1d / 1w / 1m spend and gauge the rate of change
  • forecast vs actual: DeclaredAwsCostReportSpendForecastDao projects the window ahead with a mean + confidence band, to set beside the observed trend
  • net vs gross: switch metric (UnblendedCost = gross list-price, NetUnblendedCost = net of credits) to match what "money that actually left" means for you
  • read-only + idempotent: a report has no desired state to converge — you read it; a re-read simply refreshes the derived numbers

note — Cost Explorer is not real-time (data settles over ~24–48h), so a bucket that includes today reads estimated: true. each paged read is a $0.01 Cost Explorer request. range is part of a report's identity, so it must be ABSOLUTE dates — a hardcoded window is frozen to that window; recompute the range (as above) to keep a report current.

example.8 = receive mail into s3 — an SES receipt rule that writes to a lifecycle'd bucket

SES and S3 in one wish. mail addressed to the mailbox lands as an object in the bucket, and the bucket ages it down a glacier staircase. the bucket is blocked from public access at the type level, and its policy names the exact receipt rule allowed to write into it.

import { RefByUnique } from 'domain-objects';
import {
  getDeclastructAwsProvider,
  getCredentials,
  asSesReceiptRuleArn,
  DeclaredAwsS3Bucket,
  DeclaredAwsS3BucketPolicy,
  DeclaredAwsSesReceiptRuleSet,
  DeclaredAwsSesReceiptRule,
} from 'declastruct-aws';

const BUCKET = 'ehmpathy-mail-inbound-demo';
const RULE_SET = 'ehmpathy-mail-inbound';
const RULE = 'to-s3';
const ECHO = '[email protected]';

export const getProviders = async () => [
  await getDeclastructAwsProvider({}, { log: console }),
];

export const getResources = async () => {
  const { account, region } = await getCredentials();

  // the bucket policy's SourceArn must name the RECEIPT RULE arn (deterministic from names)
  const receiptRuleArn = asSesReceiptRuleArn({
    region,
    account,
    ruleSetName: RULE_SET,
    ruleName: RULE,
  });

  // 1. the inbound store — glacier staircase lifecycle (Standard -> GLACIER_IR -> DEEP_ARCHIVE);
  //    a mail store is never public, and needs no versions
  const store = DeclaredAwsS3Bucket.as({
    name: BUCKET,
    access: { public: 'blocked' },
    lifecycle: {
      objects: {
        expire: null, // keep every current object; the transitions age it down
        transitions: [
          { afterDays: 30, class: 'GLACIER_IR' },
          { afterDays: 180, class: 'DEEP_ARCHIVE' },
        ],
      },
      versions: false, // unversioned, so this bucket stays torn-down-able
      multiparts: { expire: null },
    },
    tags: { managedBy: 'declastruct', purpose: 'mail' },
  });

  // 2. the bucket policy — allow SES (this account only) to PutObject into inbound/
  const storePolicy = DeclaredAwsS3BucketPolicy.as({
    bucket: RefByUnique.as<typeof DeclaredAwsS3Bucket>({ name: BUCKET }),
    document: {
      statements: [
        {
          sid: 'AllowSesPut',
          effect: 'Allow',
          principal: { service: 'ses.amazonaws.com' },
          action: 's3:PutObject',
          resource: `arn:aws:s3:::${BUCKET}/inbound/*`,
          condition: {
            StringEquals: { 'aws:SourceAccount': account },
            ArnLike: { 'aws:SourceArn': receiptRuleArn },
          },
        },
      ],
    },
  });

  // 3. the receipt rule set — the account's active set
  const ruleSet = DeclaredAwsSesReceiptRuleSet.as({
    name: RULE_SET,
    active: true,
  });

  // 4. the receipt rule — route inbound mail for ECHO to the s3 inbound/ prefix
  const rule = DeclaredAwsSesReceiptRule.as({
    ruleSet: RefByUnique.as<typeof DeclaredAwsSesReceiptRuleSet>({ name: RULE_SET }),
    name: RULE,
    enabled: true,
    recipients: [ECHO],
    actions: [
      {
        s3: {
          bucket: RefByUnique.as<typeof DeclaredAwsS3Bucket>({ name: BUCKET }),
          objectKeyPrefix: 'inbound/',
          topic: null,
          kmsKeyArn: null,
        },
        sns: null,
        lambda: null,
        bounce: null,
        stop: null,
        addHeader: null,
        workmail: null,
        connect: null,
      },
    ],
    // AWS materializes TlsPolicy to 'Optional' by default (it is never absent), so declare
    // the real default explicitly — a null desired would drift to a perpetual UPDATE against
    // the 'Optional' AWS returns
    tlsPolicy: 'Optional',
    scanEnabled: true,
  });

  // apply order = declared array order (declastruct does no topological sort). SES test-puts
  // the bucket when the rule is created, so the bucket + policy must exist first.
  return [store, storePolicy, ruleSet, rule];
};

this pattern enables:

  • safe by default: access: { public: 'blocked' } is one token; the public-capable posture costs four booleans typed out ({ acls: { block, ignore }, policies: { block, restrict } }), so the exposed state is never reached by inattention
  • one verb, three subjects: lifecycle decomposes by what expires — objects, versions, multiparts — each with the same expire. which sub-object is populated names the mode, so a reader sees "staircase, unversioned, no multipart cleanup" from the shape alone
  • no silent bill: multiparts.expire aborts stranded upload parts from a dead writer — parts aws s3 ls cannot see, and that bill forever otherwise
  • cross-service refs: the receipt rule names the bucket by RefByUnique, so the two converge together and a rename of either is a compile error rather than a runtime 404
  • explicit AWS defaults: tlsPolicy: 'Optional' is declared because AWS materializes it — a null desired would read UPDATE on every plan, forever

note — versions: false carries real weight for a bucket you may need to delete. once versions are enabled, a delete inserts a delete marker rather than a true erase, so the bucket cannot be emptied by object deletes alone — and 'suspended' does not undo that. s3 bucket names are a global namespace, so a stuck bucket burns its name account-wide.