Skip to content

escapace/seedpods

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

290 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

seedpods

Define cookies once as named values in application code, choose signing or encryption, provide multiple keys for rotation, and set transport attributes such as path, domain, SameSite, HttpOnly, Secure, Partitioned, and Max-Age. The library reads them from the incoming Cookie header, returns a typed cookie interface for getting, setting, refreshing, and deleting values, and emits only the changed Set-Cookie headers for the response. Existing cookie definitions support runtime updates for keys, Max-Age, HttpOnly, SameSite, Secure, and Partitioned without restarting the process. Cookie definitions can be grouped into reusable jars and combined across modules.

Install

pnpm add seedpods

Usage

A typical request and response flow has four parts: derive or load keys, assign stable identifiers to them, define cookies, and open the resulting jars for each request.

import { createCookie, createJar, deriveKey, patchCookie, useCookies } from 'seedpods'

// Derive or load keys once at startup.
const currentSessionKey = await deriveKey(process.env.SESSION_SECRET!, { salt: 'session' })
const previousSessionKey = await deriveKey(process.env.SESSION_SECRET_PREVIOUS!, {
  salt: 'session',
})
const recentViewsKey = await deriveKey(process.env.RECENT_VIEWS_SECRET!, {
  salt: 'recent-views',
})

// Wrap each key with a stable identifier. The first entry writes new cookies.
// Later entries remain readable during rotation.
const currentSessionKeyEntry = { id: 'current-session', value: currentSessionKey }
const previousSessionKeyEntry = {
  id: 'previous-session',
  value: previousSessionKey,
}
const recentViewsKeyEntry = { id: 'recent-views', value: recentViewsKey }

// Define cookies.
const sessionCookie = createCookie<'session', 'aes-gcm', { userId: string }>({
  key: 'session',
  type: 'aes-gcm',
  keys: [currentSessionKeyEntry, previousSessionKeyEntry],
  prefix: '__Host-',
  path: '/',
  secure: true,
  httpOnly: true,
  sameSite: 'Lax',
  maxAge: 60 * 60 * 24 * 7,
})

const recentViewsCookie = createCookie<'recentViews', 'hmac', string[]>({
  key: 'recentViews',
  name: 'recent-views',
  type: 'hmac',
  keys: [recentViewsKeyEntry],
  path: '/',
  secure: true,
  sameSite: 'Lax',
  maxAge: 60 * 60 * 24 * 30,
})

// Group cookies into reusable jars.
const authCookies = createJar().put(sessionCookie)
const uiCookies = createJar().put(recentViewsCookie)
const appCookies = createJar().combine(authCookies).combine(uiCookies)

export async function handleRequest(request: Request) {
  const cookies = await useCookies(request.headers.get('cookie') ?? undefined, appCookies, {
    recentViews(previous = [], next = []) {
      return [...previous, ...next].slice(-10)
    },
  })

  const signOut = new URL(request.url).pathname === '/logout'

  if (signOut) {
    cookies.del('session')
  } else if (cookies.get('session') === undefined) {
    cookies.set('session', { userId: '123' })
  }

  cookies.set('recentViews', ['/docs/getting-started'])

  const responseHeaders = new Headers()
  for (const value of await cookies.values()) {
    responseHeaders.append('Set-Cookie', value)
  }

  return new Response('ok', { headers: responseHeaders })
}

useCookies() returns only the changed Set-Cookie values through values(). Use entries() when the cookie key is also needed.

Runtime cookie settings can be updated on an existing cookie definition without recreating jars or restarting the process.

patchCookie(sessionCookie, (draft) => {
  draft.keys = [nextSessionKeyEntry, currentSessionKeyEntry, previousSessionKeyEntry]
  draft.sameSite = 'Strict'
  draft.maxAge = 60 * 60 * 24 * 14
})

The call updates later requests immediately. Existing useCookies() instances keep the request-local configuration they already captured.

Behavior notes

  • useCookies() accepts the raw Cookie header value.
  • Each jar key must be unique. Calling put with the same key twice throws.
  • Different cookie definitions may still share one cookie name when the decoded value carries a different logical key.
  • A reducer may return undefined to delete a cookie. If a reducer throws, that update is aborted and the earlier state is preserved.
  • Domain must be an ASCII host name. Internationalized domains must use their ASCII form, for example xn--bcher-kva.example instead of bücher.example.
  • partitioned: true requires secure: true. Browsers enforce partitioned storage semantics; seedpods only emits the Partitioned attribute.
  • Reading a cookie with a non-primary configured key causes the next output to rewrite it with the first configured key. Reading a cookie whose transport policy differs from the current definition rewrites it with the current maxAge, sameSite, httpOnly, secure, and partitioned attributes. Some rewrites take effect only when the user agent accepts the Set-Cookie header for that response. Under rfc6265bis, SameSite=Lax and SameSite=Strict cookies are not set in responses to cross-site subresource requests or cross-site nested navigations. Changes to name, prefix, domain, and path are not migrated automatically.
  • If a configured cookie cannot be verified or decoded, get() returns undefined and the next output expires it.
  • values() and entries() emit only changes. Deleting an already unset cookie records no new change, and unchanged values do not produce a Set-Cookie header unless refresh() is called explicitly. Use refresh(key) for sliding sessions and other flows that need to rewrite the current value to extend browser-managed lifetime such as Max-Age.

API

function assertCookie

Asserts that a value was created by createCookie.

export declare function assertCookie(
  cookie: unknown,
): asserts cookie is SeedpodsCookie<string, SeedpodsCookieType, unknown>

Parameters

Parameter Type Description
cookie
unknown
Value to validate.

Throws

SeedpodsError When the value is not a cookie definition created by this package.

function assertJar

Asserts that a value was created by createJar.

export declare function assertJar(value: unknown): asserts value is SeedpodsJarInterface

Parameters

Parameter Type Description
value
unknown
Value to validate.

Throws

SeedpodsError When the value is not a cookie jar created by this package.

function createCookie

Creates a cookie definition for one application value.

createCookie: <T extends string, U extends SeedpodsCookieType, V>(
  options: SeedpodsCookieOptionsForType<T, U>,
) => SeedpodsCookie<T, U, V>

Type Parameters

Parameter Description
T Key used to read, write, and delete this cookie through the cookie interface.
U Cookie protection mode.
V Application value stored in the cookie.

Parameters

Parameter Type Description
options
SeedpodsCookieOptionsForType<T, U>
Cookie configuration, including the logical cookie key, configured cryptographic keys, and transport attributes.

Returns

A cookie definition that can be added to a jar.

Throws

When the cookie options are invalid.

Remarks

Use this function to declare how a cookie is named, protected, and serialized. The returned definition can be added to a jar created by createJar and later used by useCookies to read incoming cookies and produce Set-Cookie header values.

function createJar

Creates an empty cookie jar.

createJar: () => SeedpodsJarBuilder<SeedpodsJar<SeedpodsJarEmptyState>, 'combine' | 'put'>

Returns

An empty cookie jar with put and combine operations.

Remarks

Use put to add cookie definitions and combine to merge another jar. In TypeScript, each call returns a new jar type that keeps the available cookie keys aligned with the configured definitions.

function getSeedpodsErrorCausesByType

Returns the causes from a SeedpodsError that match the given type.

export declare function getSeedpodsErrorCausesByType<T extends SeedpodsErrorType>(
  error: SeedpodsError,
  type: T,
): Array<SeedpodsErrorCause<T>>

Type Parameters

Parameter Description
T Error cause type to match.

Parameters

Parameter Type Description
error
SeedpodsError
Error instance to inspect.
type
T
Error cause type to match.

Returns

All matching causes in their original order.

function isSeedpodsError

Checks whether a value is a SeedpodsError.

export declare function isSeedpodsError(value: unknown): value is SeedpodsError

Parameters

Parameter Type Description
value
unknown
Value to test.

Returns

true when the value is a SeedpodsError; otherwise, false.

function isSeedpodsErrorOfType

Checks whether a SeedpodsError contains at least one cause of the given type.

export declare function isSeedpodsErrorOfType<T extends SeedpodsErrorType>(
  error: SeedpodsError,
  type: T,
): error is SeedpodsError<T>

Type Parameters

Parameter Description
T Error cause type to match.

Parameters

Parameter Type Description
error
SeedpodsError
Error instance to inspect.
type
T
Error cause type to match.

Returns

true when the error contains at least one matching cause; otherwise, false.

function patchCookie

Updates the hot-patchable runtime fields of an existing cookie definition.

patchCookie: <T extends string, U extends SeedpodsCookieType, V>(cookie: SeedpodsCookie<T, U, V>,
  recipe: (draft: SeedpodsCookieRuntimeDraft) => void) => void

Type Parameters

Parameter Description
T Application key used to address the cookie through the cookie interface.
U Cookie protection mode.
V Application value stored in the cookie.

Parameters

Parameter Type Description
cookie
SeedpodsCookie<T, U, V>
Cookie definition created by createCookie.
recipe
(draft: SeedpodsCookieRuntimeDraft) => void
Callback that mutates the runtime draft before the next configuration is validated and committed.

Throws

SeedpodsError When cookie was not created by this package or when the patched runtime options are invalid.

Remarks

This function can rotate configured keys and update maxAge, httpOnly, sameSite, secure, and partitioned without recreating the cookie definition. It does not change cookie identity or browser scope fields such as key, type, name, prefix, domain, or path.

The update is committed before the function returns. Later requests observe the new runtime immediately. Existing useCookies instances keep the request-local configuration they captured when they were created.

Scope follows the cookie object returned by createCookie. When the same cookie object is reused across multiple jars, one call updates all jars that reference it. When validation fails, the function throws and leaves the previous runtime unchanged.

function useCookies

Creates a cookie interface from a Cookie header value or parsed cookie map and a cookie jar.

useCookies: <SeedpodsJar extends SeedpodsJarInterface>(
  cookieHeader: SeedpodsCookieHeader,
  jar: SeedpodsJar,
  reducers?: SeedpodsCookiesReducers<SeedpodsJar>,
) => Promise<SeedpodsCookies<SeedpodsJar>>

Type Parameters

Parameter Description
SeedpodsJar Jar type that defines the available cookie keys and value types.

Parameters

Parameter Type Description
cookieHeader
SeedpodsCookieHeader
Incoming Cookie header value or map returned by parseCookieHeader. When omitted, the interface starts with no received cookies.
jar
SeedpodsJar
Cookie definitions created with createJar.
reducers
SeedpodsCookiesReducers<SeedpodsJar>
Optional reducers that combine the current cookie value with a later value passed to set.

Returns

A cookie interface for reading values, recording changes, and generating changed Set-Cookie header values.

Remarks

The returned interface reads configured cookie values through get, records changes through set, del, and refresh, and produces changed Set-Cookie header values through entries and values. If the header contains the same cookie name more than once, the function keeps the best decodable value for each configured cookie.

function deriveKey

Derives a symmetric key from a secret string.

deriveKey: (secret: string, options?: SeedpodsDeriveKeyOptions) => Promise<Uint8Array>

Parameters

Parameter Type Description
secret
string
Secret input used as the derivation source.
options
SeedpodsDeriveKeyOptions
Optional derivation parameters, including salt and iteration count.

Returns

The derived key bytes.

Remarks

The function uses Password-Based Key Derivation Function 2 with SHA-512 and returns raw key bytes. Pass a fixed salt when the same secret must produce the same key across processes or deployments. When no salt is provided, a random salt is generated and the derived key changes between calls.

The returned bytes are algorithm-agnostic. The WebCrypto API requires an algorithm label during derivation; internally the function uses AES-GCM as that label, but the exported raw bytes are suitable for both aes-gcm and hmac cookie types.

function parseCookieHeader

Parses a Cookie header value into a map of cookie names and values.

export declare function parseCookieHeader(string?: string): Map<string, string[]>

Parameters

Parameter Type Description
string
string
Raw Cookie header value.

Returns

A map from each cookie name to all received values for that name.

Remarks

Repeated cookie names are preserved in encounter order. Quoted cookie values keep their surrounding double quotes because rfc6265bis treats them as part of the cookie-value. Fragments without an equals sign are ignored. When the input is undefined, the function returns an empty map.

class SeedpodsError

Error thrown for invalid cookie definitions, invalid cookie values, invalid jar values, and other rejected inputs.

export declare class SeedpodsError<T extends SeedpodsErrorType = SeedpodsErrorType> extends Error

Type Parameters

Parameter Description
T Error cause type carried by the instance.

Remarks

The causes property stores machine-readable error details. Validation may report more than one cause in a single error.

new SeedpodsError

Constructs a new instance of the SeedpodsError class

constructor(causes: ReadonlyArray<SeedpodsErrorCause<T>>);

Parameters

Parameter Type
causes
ReadonlyArray<SeedpodsErrorCause<T>>

interface SeedpodsConfiguredKey

Configured cryptographic key used to sign or encrypt cookies.

export interface SeedpodsConfiguredKey

Remarks

The first configured key writes new cookies. Later keys remain readable during rotation. The identifier is written into the cookie as non-secret metadata so the matching configured key can be selected directly.

SeedpodsConfiguredKey.id

Stable identifier for this configured key.

id: string

SeedpodsConfiguredKey.value

Raw key bytes.

value: Uint8Array

interface SeedpodsCookie

Cookie definition returned by createCookie.

export interface SeedpodsCookie<T extends string = any, U extends SeedpodsCookieType = any, _V = any>

Type Parameters

Parameter Description
T Application key used to address the cookie.
U Cookie protection mode.
_V Application value stored in the cookie.

Remarks

Most callers create values of this type through createCookie and then add them to a jar with createJar.

interface SeedpodsCookieOptionsBase

Common cookie options shared by all cookie definitions.

export interface SeedpodsCookieOptionsBase<SeedpodsCookieKey extends string = string>

Type Parameters

Parameter Description
SeedpodsCookieKey Application key used to address the cookie through the cookie interface.

SeedpodsCookieOptionsBase.domain

Domain attribute written to Set-Cookie.

domain?: string;

SeedpodsCookieOptionsBase.httpOnly

Whether the cookie is inaccessible to client-side JavaScript.

httpOnly?: boolean;

SeedpodsCookieOptionsBase.key

Application key used to read, write, and delete the cookie.

key: SeedpodsCookieKey

SeedpodsCookieOptionsBase.maxAge

Max-Age attribute, in seconds.

maxAge?: number;

SeedpodsCookieOptionsBase.name

Cookie name written to the header. When omitted, the key is used.

name?: string;

SeedpodsCookieOptionsBase.partitioned

Whether the cookie should opt in to partitioned storage where supported.

partitioned?: boolean;

Remarks

partitioned: true requires secure: true.

SeedpodsCookieOptionsBase.path

Path attribute written to Set-Cookie.

path?: string;

SeedpodsCookieOptionsBase.prefix

Optional cookie name prefix.

prefix?: SeedpodsCookiePrefix;

SeedpodsCookieOptionsBase.sameSite

SameSite attribute written to Set-Cookie.

sameSite?: SeedpodsCookieSameSite;

Remarks

sameSite: 'None' requires secure: true.

SeedpodsCookieOptionsBase.secure

Whether the cookie requires a secure transport.

secure?: boolean;

interface SeedpodsCookies

Mutable cookie interface returned by useCookies.

export interface SeedpodsCookies<SeedpodsJarType extends SeedpodsJarInterface>

Type Parameters

Parameter Description
SeedpodsJarType Jar type that defines the available cookie keys and value types.

Remarks

Call set, del, or refresh to record deliberate cookie writes. Calling refresh rewrites the current cookie value without changing its logical value.

SeedpodsCookies.del

Records deletion of one cookie key.

del: (key: SeedpodsJarKeys<SeedpodsJarType>) => void;

SeedpodsCookies.entries

Returns changed cookie keys and their Set-Cookie header values.

entries: () => Promise<Array<[SeedpodsJarKeys<SeedpodsJarType>, string]>>

SeedpodsCookies.get

Returns the current value for one cookie key.

get: <SeedpodsKey extends SeedpodsJarKeys<SeedpodsJarType>>(key: SeedpodsKey) =>
  SeedpodsJarCookieValue<SeedpodsJarType, SeedpodsKey> | undefined

SeedpodsCookies.refresh

Rewrites the current value for one cookie key without changing that value.

refresh: (key: SeedpodsJarKeys<SeedpodsJarType>) => void;

Remarks

This is useful for renewing browser-managed lifetime such as Max-Age when the logical value stays the same.

SeedpodsCookies.set

Records a new value for one cookie key.

set: <SeedpodsKey extends SeedpodsJarKeys<SeedpodsJarType>>(key: SeedpodsKey,
  value: SeedpodsJarCookieValue<SeedpodsJarType, SeedpodsKey> | undefined) => void;

SeedpodsCookies.values

Returns changed Set-Cookie header values.

values: () => Promise<string[]>

interface SeedpodsDeriveKeyOptions

Options for deriveKey.

export interface SeedpodsDeriveKeyOptions

SeedpodsDeriveKeyOptions.iterations

Password-Based Key Derivation Function 2 iteration count.

iterations?: number;

SeedpodsDeriveKeyOptions.length

Requested output length, in bytes.

length?: number;

Remarks

The current implementation always derives a 256-bit key.

SeedpodsDeriveKeyOptions.salt

Salt used for key derivation.

salt?: string;

interface SeedpodsEncryptedCookieOptions

Options for an encrypted cookie definition.

export interface SeedpodsEncryptedCookieOptions<SeedpodsCookieKey extends string = string>
  extends SeedpodsCookieOptionsBase<SeedpodsCookieKey>

Type Parameters

Parameter Description
SeedpodsCookieKey Application key used to address the cookie through the cookie interface.

Remarks

The first configured key writes new cookies. Later keys remain readable during rotation.

SeedpodsEncryptedCookieOptions.keys

Configured encryption keys in primary-first order.

keys: SeedpodsConfiguredKey[];

SeedpodsEncryptedCookieOptions.type

Encryption mode.

type: 'aes-gcm'

interface SeedpodsErrorMetadata

Structured data carried by each SeedpodsError cause type.

export interface SeedpodsErrorMetadata

interface SeedpodsJar

Cookie jar returned by createJar.

export interface SeedpodsJar<State extends SeedpodsJarState> extends SeedpodsJarInterface<State>

Type Parameters

Parameter Description
State Jar state tracked by the type system.

Remarks

Most callers build this type through chained put and combine calls rather than constructing it directly.

SeedpodsJar.combine

Merges another cookie jar into this one.

combine: <SeedpodsChildJar extends SeedpodsJar<SeedpodsJarState>>(jar: SeedpodsChildJar) =>
  SeedpodsJarBuilder<
    SeedpodsJar<SeedpodsJarStateAfterAction<State, SeedpodsJarCombineAction<SeedpodsChildJar>>>,
    'combine' | 'put' | typeof SEEDPODS_SYMBOL_JAR
  >

SeedpodsJar.put

Adds one cookie definition to the jar.

put: <SeedpodsCookieTypeValue extends SeedpodsCookie>(cookie: SeedpodsCookieTypeValue) =>
  SeedpodsJarBuilder<
    SeedpodsJar<
      SeedpodsJarStateAfterAction<State, SeedpodsJarCookieAction<SeedpodsCookieTypeValue>>
    >,
    'combine' | 'put' | typeof SEEDPODS_SYMBOL_JAR
  >

interface SeedpodsSignedCookieOptions

Options for a signed cookie definition.

export interface SeedpodsSignedCookieOptions<SeedpodsCookieKey extends string = string>
  extends SeedpodsCookieOptionsBase<SeedpodsCookieKey>

Type Parameters

Parameter Description
SeedpodsCookieKey Application key used to address the cookie through the cookie interface.

Remarks

The cookie value remains readable by clients. The signature prevents undetected modification.

SeedpodsSignedCookieOptions.keys

Configured signing keys in primary-first order.

keys: SeedpodsConfiguredKey[];

SeedpodsSignedCookieOptions.type

Signing mode.

type: 'hmac'

type SeedpodsCookieHeader

Cookie header input accepted by useCookies.

export type SeedpodsCookieHeader = string | SeedpodsParsedCookieHeader | undefined

Remarks

Pass a string to let seedpods parse the header, pass a parsed map to filter or otherwise adjust cookie values before opening a jar, or pass undefined to start with no received cookies.

type SeedpodsCookieOptions

Supported options accepted by createCookie.

export type SeedpodsCookieOptions<SeedpodsCookieKey extends string = string> =
  | SeedpodsEncryptedCookieOptions<SeedpodsCookieKey>
  | SeedpodsSignedCookieOptions<SeedpodsCookieKey>

Type Parameters

Parameter Description
SeedpodsCookieKey Application key used to address the cookie through the cookie interface.

type SeedpodsCookiePrefix

Supported cookie name prefixes for createCookie.

export type SeedpodsCookiePrefix = (typeof SEEDPODS_COOKIE_PREFIXES)[number]

Remarks

__Secure- requires secure: true. __Host- requires secure: true, path: '/', and no domain.

type SeedpodsCookieSameSite

Supported SameSite attribute values for cookie definitions.

export type SeedpodsCookieSameSite = (typeof SEEDPODS_COOKIE_SAME_SITE_VALUES)[number]

type SeedpodsCookiesReducer

Reducer used to combine the current and next value for one cookie key.

export type SeedpodsCookiesReducer<SeedpodsValue> = (
  previous?: SeedpodsValue,
  next?: SeedpodsValue,
) => SeedpodsValue | undefined

Type Parameters

Parameter Description
SeedpodsValue Cookie value type.

type SeedpodsCookiesReducers

Reducer map accepted by useCookies.

export type SeedpodsCookiesReducers<SeedpodsJarType extends SeedpodsJarInterface> = {
  [SeedpodsKey in SeedpodsJarKeys<SeedpodsJarType>]?:
    | SeedpodsCookiesReducer<SeedpodsJarCookieValue<SeedpodsJarType, SeedpodsKey>>
    | undefined
}

Type Parameters

Parameter Description
SeedpodsJarType Jar type that defines the available cookie keys and value types.

type SeedpodsCookieType

Supported cookie protection modes.

export type SeedpodsCookieType = (typeof SEEDPODS_COOKIE_TYPES)[number]

Remarks

'aes-gcm' encrypts and authenticates the cookie value. 'hmac' signs the value without encrypting it.

type SeedpodsErrorCause

Machine-readable error detail carried by SeedpodsError.

export type SeedpodsErrorCause<T extends SeedpodsErrorType = SeedpodsErrorType> = T
  extends SeedpodsErrorType ? {
  type: T;
} & SeedpodsErrorMetadata[T] : never;

Type Parameters

Parameter Description
T Error cause type to project.

type SeedpodsErrorType

Supported SeedpodsError cause types.

export type SeedpodsErrorType = (typeof SEEDPODS_ERROR_TYPES)[number]

type SeedpodsParsedCookieHeader

Parsed Cookie header value accepted by useCookies.

export type SeedpodsParsedCookieHeader = ReadonlyMap<string, readonly string[]>

Remarks

This is the return shape of parseCookieHeader. Each map key is a cookie name, and each value preserves all received values for that name in encounter order.

About

Define reusable signed or encrypted cookies once, then read them from Cookie headers and emit changed Set-Cookie headers through a typed interface.

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors