Skip to content

@j0nathan-ll0yd/types ​

The framework's shared type vocabulary: branded wrappers for AWS identifiers that are plain strings at runtime.

This is the lowest layer of the @j0nathan-ll0yd/* dependency graph. It imports from no other framework package, which is what lets every other package agree on these identifiers without a cycle. @j0nathan-ll0yd/core re-exports all of it, so handler code usually imports from core rather than from here.

Why brand a string ​

exportToS3(bucket, key) takes two strings. Nothing stops a caller passing them in the wrong order, and the failure surfaces at runtime in a deployed Lambda. A branded type makes that a compile error at no runtime cost: the brand is a phantom property that TypeScript erases.

typescript
import { S3BucketName } from "@j0nathan-ll0yd/types";
import { getRequiredEnv } from "@j0nathan-ll0yd/env";

const bucket = S3BucketName(getRequiredEnv("DATA_BUCKET"));
await exportToS3({ bucket, key: "report.json", body });

await exportToS3({ bucket: "report.json", key: bucket, body }); // compile error

Wrap once, at the point the value enters the program -- normally the environment read. Do not wrap at each call site.

Brand ​

typescript
type Brand<T, B extends string> = T & { readonly [__brand]: B };

The utility every branded type is built from. __brand is a unique symbol that exists only in the type system.

Do not redeclare this pattern in an instance repo (convention C86). Branded types live in this package and nowhere else; a local copy produces two mutually incompatible S3BucketName types.

Branded types ​

Each name below is both a type and a constructor function of the same name. The constructor is an identity function -- it asserts the brand and returns the string unchanged.

Constructor / typeWraps
S3BucketName(value)An S3 bucket name.
EventBusName(value)An EventBridge event bus name.
LambdaArn(value)A Lambda function ARN.
BearerToken(value)A JWT or static bearer token.
ApiGatewayRouteKey(value)An API Gateway route key, e.g. $connect.
DsqlClusterArn(value)An Aurora DSQL cluster ARN.
SqsQueueUrl(value)An SQS queue URL.
CloudFrontDistributionId(value)A CloudFront distribution ID.
typescript
import {
  ApiGatewayRouteKey,
  BearerToken,
  CloudFrontDistributionId,
  DsqlClusterArn,
  EventBusName,
  LambdaArn,
  S3BucketName,
  SqsQueueUrl,
} from "@j0nathan-ll0yd/types";

DeploymentProvenance ​

The deployment facts mantle deploy derives and binds into a Lambda's environment. Framework-owned because the framework derives them; contract identity (spec shas, rule versions, artifact key lists) stays app-owned.

typescript
interface DeploymentProvenance {
  stage: string;
  sourceRevision: string;
  deployedAt?: string;
  workflowRef?: string;
  workflowRunId?: string;
  workflowRunAttempt?: string;
}

deployedAt and the workflow fields are optional so one type carries the honest subset: a writer with no contract rule to cite stamps the revision alone. Loaders live in @j0nathan-ll0yd/core; the writer that stamps them is exportToS3.

Artifact declarations ​

One published artifact's identity: its object key, its wire encoding, and the publication options that belong to the artifact rather than to a call site. They live in this package because it is the only @j0nathan-ll0yd/* layer with no dependencies at all, so an application's artifact module stays a pure module — importable by build tooling with no cloud client, no credential chain, and no environment initialization — while the same module is what the runtime publication call binds to.

The framework owns encoding mechanics and owns none of the meaning. The key, the media type, the acceptable values, and the copy are all the application's. No artifact name, schema, threshold, or selector belongs here.

typescript
interface ArtifactFinding {
  readonly id: string;
  readonly message: string;
}

type ArtifactVerdict =
  | { readonly ok: true; readonly warnings: readonly ArtifactFinding[] }
  | { readonly ok: false; readonly issues: readonly ArtifactFinding[] };

// A codec supplies exactly one contract slot. The union is what makes both-or-neither
// a compile error rather than a runtime one.
type ArtifactCodec<TValue> = ValidatingArtifactCodec<TValue> | JudgingArtifactCodec<TValue>;

type ValidatingArtifactCodec<TValue> = {
  encode: (value: TValue) => string;
  validate: (body: string) => void;
  judge?: undefined;
};

type JudgingArtifactCodec<TValue> = {
  encode: (value: TValue) => string;
  judge: (body: string) => ArtifactVerdict;
  validate?: undefined;
};

interface ArtifactDefinition<TValue, TKey extends string = string> {
  readonly key: TKey;
  readonly contentType: string;
  readonly cacheControl?: string;
  readonly codec: ArtifactCodec<TValue>;
}

A contract slot is required, and exactly one of them. The checked publication path's only claim is that the bytes it wrote satisfied a contract, and an optional slot would let a caller obtain that claim while supplying none. A write that genuinely has nothing to check belongs on exportToS3, which promises storage and no contract. prepareArtifact refuses both-or-neither at runtime as well, naming the artifact key, because a JavaScript caller reaches the same slot with no type in the way.

jsonCodec and textCodec both produce a ValidatingArtifactCodec. A judging codec is written as an object literal today.

Choosing a slot ​

validate throws and returns nothing. judge returns what it decided: ok: true publishes and its warnings ride the PublishedArtifact receipt, ok: false refuses the write and names its issues. Reach for judge when the contract has something worth saying that should not stop a publication — a consumer that wants advisory findings reads them off the result instead of re-running the grammar outside the codec.

The explicit result also carries the rule the void signature could not:

ShapeUnder validate: (body) => voidUnder judge: (body) => ArtifactVerdict
async (body) => {...}compiles; refused at the writecompile error
function* (body) {...}compiles; refused at the writecompile error
(body) => schema.safeParse(body).successcompiles; refused at the writecompile error
(body) => { assert(body) }correctcompile error (returns no verdict)
(body) => { void check(body).catch(noop); return {ok: true, warnings: []} }n/acompiles and publishes — refused by lint

The last row is the one shape neither the type nor a return-shape check can separate from a real clean judgment, which is why local-rules/artifact-validator-sync covers the judge slot as well as validate.

validate must be synchronous, and signals by throwing. Both halves have a failure mode worth stating.

TypeScript's void return position accepts anything, so async (body) => {...}, function* (body) {...}, and a predicate all type-check there and always will — the type cannot carry this rule. prepareArtifact therefore inspects what validate returns and refuses the three shapes that prove nothing was judged: a thenable, a suspended iterator, and a boolean over a body that is not itself a JSON boolean.

A returned value is otherwise not read as a verdict. Returning the parsed value is the normal shape — schema.parse(wire) — and a legitimate parse can return 0 or '', so treating a falsy return as invalid would reject conformant artifacts. A parse returns a boolean only when the artifact IS a boolean, and jsonCodec serializes those two artifacts to exactly true and false, which is what separates them from the predicate form:

typescript
// WRONG: returns false for an invalid body and never throws. Refused at the write.
jsonCodec<BooksExport>((wire) => booksSchema.safeParse(wire).success);

// WRONG: returns undefined, exactly as a conformant validator does, so the write CANNOT see it.
// `local-rules/artifact-validator-sync` refuses this one at authoring time instead.
jsonCodec<BooksExport>((wire) => {
  void checkAsync(wire).catch(logError);
});

// Right: assert, so a violation throws.
jsonCodec<BooksExport>((wire) => booksSchema.parse(wire));

// Also right: throw explicitly, keeping the error the contract owner wants reported.
jsonCodec<BooksExport>((wire) => {
  const result = booksSchema.safeParse(wire);
  if (!result.success) {
    throw new Error(`books.json violates books-export.schema.json: ${result.error.message}`);
  }
});

defineArtifact(definition) ​

Declare an artifact, inferring its value type from its codec and narrowing its key to a literal. An identity function: it adds no behavior and reads no environment, so a module of these declarations stays pure and statically readable, and mantle generate dataflow projects the key and media type from it without loading or executing anything.

typescript
import { defineArtifact, jsonCodec, textCodec } from "@j0nathan-ll0yd/types";
import catalogSchema from "../schemas/catalog.schema.json" with { type: "json" };
import { assertMatchesSchema } from "./services/catalogContract.js";

export const ARTIFACTS = {
  catalog: defineArtifact({
    key: "catalog.json",
    contentType: "application/json",
    cacheControl: "max-age=30, s-maxage=30",
    codec: jsonCodec<CatalogExport>((wire) => assertMatchesSchema(catalogSchema, wire)),
  }),
  llmsTxt: defineArtifact({
    key: "llms.txt",
    contentType: "text/markdown; charset=utf-8",
    codec: textCodec(assertLlmsTxtGrammar),
  }),
} as const;

jsonCodec(validate) ​

A JSON wire encoding whose validator judges the serialization result. validate receives JSON.parse(JSON.stringify(value)), never the original object.

The re-parse is load-bearing rather than defensive. JSON.stringify drops undefined-valued keys, turns a Date into a string, and turns NaN and Infinity into null, so a validator handed the original object would accept payloads the served file violates and reject a Date that serializes conformantly. What counts as conformant is entirely the caller's; this package holds no schema catalog.

textCodec(validate) ​

A text wire encoding that publishes its value byte for byte. encode is the identity, so a composed document reaches the object store exactly as the application rendered it — no re-encoding, no JSON quoting, no media type the framework chose.

  • @j0nathan-ll0yd/core -- re-exports the branded types, Brand, and DeploymentProvenance. It does NOT re-export the artifact declarations: an artifact module imports defineArtifact, jsonCodec, and textCodec from this package directly, which is what keeps it free of every dependency core carries.
  • Artifact assurance pilot -- how these declarations pair with the checked publication path, and what it does and does not guarantee.
  • Deployment Provenance -- how these facts are derived, bound, and stamped.
  • Resource Binding -- how the CLI wires the environment variables these brands wrap.