inkan

Sessions

Cookie sessions: signed, encrypted, or a random id in front of a store.

Needs inkan 0.8

@inkanjs/session 0.2 needs inkan 0.8.0 or newer, below 1.0. It keeps sessions in any inkan Store, with TTLs in milliseconds.

npm i @inkanjs/session
import {  } from "@inkanjs/session";

type  = { : number; : string[] };


  .(<>({ : ..! }))
  .("/login", { : .({ : .(), : .() }) }, async () => {
    const  = await checkPassword(.., ..);
    .session.regenerate().set("userId", .id); // a new id at sign-in
    return { : true };
  })
  .("/me", () => ({ : .session.get("userId") ?? null }))
  .("/logout", () => {
    .session.destroy();
    return { : true };
  });

ctx.session is typed by the Data you give it, made only for a request that reads it, and written back only when it changed. No dependencies: everything is node:crypto. Make a secret with node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))".

Three ways to keep it

the cookie holdsthe client can read itrevocableroom
signed (default)the data and an HMAC-SHA256yesno, valid until it expiresabout 3 KB
encrypt: truethe data, AES-256-GCMnono, valid until it expiresabout 2.5 KB
storea random 256-bit id, signednoyes: destroy() deletes itwhat the store takes

A signed cookie cannot be changed, but anyone who has it can read it: keep nothing secret in it. A cookie session lasts until it expires even after destroy(), if someone kept a copy. When logout must end a session for certain, use a store.

import { memoryStore } from "@vxnsin/inkan";
import { session } from "@inkanjs/session";

app.register(session({ secret, store: memoryStore({ max: 50_000 }) }));

The store is an inkan Store. memoryStore from @vxnsin/inkan lives in the process: right for development and one process. For more, any Store works, such as Redis, PostgreSQL, MongoDB or MySQL:

import { redisStore } from "@inkanjs/redis";

app.register(session({ secret, store: redisStore(redis) }));

The plugin uses the store's get, set and delete, keeps one record per session ({ d, c, f? }, plain JSON), and gives it a TTL of cookie.maxAge in milliseconds. A store that is not a Store is refused when the plugin is made.

ctx.session

get(key), has(key)read, typed by Data
set(key, value), delete(key), clear()change; the answer carries the new cookie
regenerate()a new id for the same data. Call it at sign-in: an id somebody planted or saw before is worth nothing afterwards (session fixation)
destroy()ends it: the cookie is cleared, the store forgets the id
flash(type, message), flashes(type?)messages for a later request, read once (below)
id, isNew, changed, toJSON()

Flash messages

A message for the next page, such as "Saved." after a form posts and redirects. It stays in the session until it is read, and is gone once it was.


  .("/teas", async () => {
    await saveTea(.);
    .session.flash("success", "Tea saved.");
    return .("/teas", 303);
  })
  .("/teas", () => {
    const  = .session.flashes("success"); // ["Tea saved."], then [] on the next request
    return .(teaList());
  });
  • flash(type, message) adds a message of a type ("success", "error", …); messages of a type are kept in order. Both are strings.
  • flashes(type) hands back the messages of that type and removes them; flashes() all of them by type, { success: ["Tea saved."] }.
  • They are part of the session: in the cookie, signed or encrypted, or in the store, so the cookie's size limit counts them too. clear() and destroy() drop them. Reading when there are none sets no cookie.
  • Render them escaped, as the html tag does: a message may carry what a user typed.

Behaviour

  • CSRF is not handled here. The cookie is SameSite=Lax by default, which keeps it off cross-site POSTs from other sites in current browsers, and that is all. Lax still sends it on top-level GET navigations, and same-site subdomains are not other sites. If a form or a fetch changes something on the strength of this cookie, check the request's origin: use @inkanjs/auth, which does, a check of Sec-Fetch-Site / Origin of your own, or sameSite: "strict".
  • Rolling sessions never end while used, unless you give maxLifetime: then a session ends that long after it began (or after the last regenerate()), however active it was.
  • A request that never reads ctx.session costs nothing and sets no cookie. One that reads but does not change it sets none either, unless rolling.
  • An answer that sets the cookie gets cache-control: private, no-store unless the handler set its own, so no shared cache keeps one user's cookie for another.
  • A changed, forged, expired or foreign cookie is no session: the request gets a new, empty one. Every value is bound to the cookie's name.
  • Secrets rotate: the first in the list signs and encrypts, every one is accepted. Put the new one first; drop the old one once maxAge has passed. Each secret gives two keys through HKDF-SHA256, one to sign and one to encrypt.
  • A cookie over 4096 bytes is refused with an error (a 500) instead of being sent: browsers drop it without a word. Keep less in the session, or use a store.
  • With a store, the session is read in an onRequest hook, and only when the request brings a valid cookie. A forged id never reaches the store.

Options

optiondefault
secretrequiredat least 32 bytes (a string as UTF-8, or bytes); a list rotates them
encryptfalseAES-256-GCM instead of a signature; ignored with a store
storenoneany inkan Store; the TTLs it gets are milliseconds, records plain JSON
rollingfalsewrite the cookie on every answer, so a session lasts maxAge after the last request
maxLifetimenoneseconds a session lasts at most from its start (or regenerate()), however active; with rolling you want one
cookie.name"sid"
cookie.maxAge86400seconds; checked on the server too, not only by the browser
cookie.securetrueonly over HTTPS; browsers take it on http://localhost as well
cookie.sameSite"lax""strict", "lax" or "none" (which needs secure)
cookie.httpOnlytrue
cookie.path"/"
cookie.domainnone

OpenAPI

Unchanged. It is a shared plugin: registered on the app it serves every route, inside a plugin only that plugin's routes.

On this page