inkan

Context

ctx: everything a handler, a hook and a middleware get about their request.

.("/teas/:id", { : .({ : .() }) }, () => {
  .("cache-control", "max-age=60");
  return findTea(..);
});

Every handler gets the context of its request. It can be taken apart, ({ params, header }) => …, and every part of it still works: the functions on it need no this. Nothing on it is made before it is asked for, so what a handler does not use costs nothing.

params, query, headers, body

.("/teas/:shop", { : .({ : .() }), : NewTea }, ({ ,  }) =>
  store.add(., ),
);

The input, checked and typed by the route's schemas. Without a schema they are the request's own values, as text.

rawHeaders

({  }) => ["sec-fetch-site"]

The request's headers as they arrived, names in lower case, even where a route's header schema cut ctx.headers down to its contract. For hooks and plugins that must see a header no route lists. It is the request's own object, not a copy: read it, do not change it. New in 0.7.0.

method, path, url, httpVersion

({ , , ,  }) => `${} ${} on ${.host} over HTTP/${}`

The request line. url is a URL, made only when it is read; its scheme is protocol. httpVersion is "1.1", "2.0" or "1.0", and "1.1" where the platform does not say (app.fetch). httpVersion is new in 0.8.0.

secure, protocol

({ ,  }) => ("sid", id, {  })
({ ,  }) => `${protocol}://${headers.host}/teas`

Whether the request came over TLS, and "https" or "http" accordingly: the TLS socket under listen, the URL's scheme under app.fetch, secure on an adapter's request. With trustProxy, what the trusted proxies forward in x-forwarded-proto or forwarded's proto=. New in 0.7.0.

id

({  }) => logger.info({  }, "tea added")

The request id: the one in x-request-id when it looks safe, a fresh one otherwise. It goes back out as a header and sits in every log line and in every problem document, so a bug report points at the right line.

ip

({  }) => ({ :  })

The client's address, as the socket or the platform says it. Behind a proxy that is the proxy, unless the app says trustProxy: then it is the client's address from x-forwarded-for or forwarded, believed only as far as the proxies you named.

cookies

({  }) => sessions.get(.sid)

The request's cookies, by name, decoded. Read on first use.

signal

.("/report", { : 600_000 }, ({  }) => db.query(REPORT, {  }));

An AbortSignal that aborts when the client goes away before the answer is out, or when the route's timeout runs out. Hand it to the database driver or to fetch, and work nobody waits for any more stops.

state

.(async (, ) => {
  .. = await userFrom(..authorization);
  await ();
});

Free space for middleware to hand things on to the handler. For values that are the same on every request, a decoration is cheaper.

import {  } from "@vxnsin/inkan";

export const  = <>("tenant");

.(() => .(, lookUp()));
.("/me", () => .()?.name); // typed: Tenant | undefined

stateKey<T>(name) is a typed slot in ctx.state, under a symbol of its own: two plugins that pick the same name never meet, no string key reaches it, and ctx.state stays a plain record for everyone else. Make it once, at the top of a module, and export it to whoever reads it. Experimental, new in 0.8.0.

route

({  }) => metrics.count(?.path) // "/teas/:id", not "/teas/7"

The route that matched, as it was written: its method, its path, the security it asks for (its own or its group's, empty for none) and its meta. One object per route, made before the first request, so hooks can read it for free.

req, res

Node's request and response, when the request came through a socket. Writing to res yourself is allowed; inkan then leaves the answer alone.

status()

({  }) => {
  (202);
  return { : true };
};

The status a plain value answers with.

({  }) => {
  ("cache-control", "no-store");
  return data;
};

A header on the answer.

reply()

.("/teas", { : NewTea, : { 201: Tea } }, ({ ,  }) => {
  const  = store.add();
  return (201, , { : `/teas/${.id}` });
});

Another status, with extra headers if needed. It is typed to the statuses the contract lists, so reply(418, …) on a route that never promised a teapot does not compile.

return (200, body, { "set-cookie": ["a=1; Path=/", "b=2; Path=/; HttpOnly"] });

A list for set-cookie goes out as a line per entry, next to the cookies setCookie() made, so a proxy can pass an upstream's cookies on as they came. A list for any other header is joined with ", ". The headers' type is exported as ReplyHeaders. New in 0.7.0.

text()

({  }) => ("pong")

Plain text. The status is the one status() set, or the usual one, unless given: text("gone", 410).

html()

import {  } from "@vxnsin/inkan";

.("/tea/:name", ({ , :  }) => (`<h1>${.}</h1>`));
.("/hi/:name", ({  }) => `<p>Hello, ${.}</p>`); // the same, returned as it is

HTML. The html tag escapes every value put into it, so what a user typed cannot become a script on the page. Lists are joined, and null, undefined and false write nothing:

`<ul>${teas.map(() => `<li>${.name}</li>`)}</ul>${admin && `<a href="/admin">admin</a>`}`

raw(markup) lets markup you made yourself through unescaped. Never use it for anything a user sent.

render()

.((, {  }) => `<!doctype html><title>${}</title><main>${}</main>`);
.("/", ({  }) => (`<p>Tea, checked.</p>`, { : "Home" }));

HTML in the layout of the route's scope, with the props handed to the layout. A plugin can have a layout of its own; the closest one wins. Without a layout, the content goes out as it is.

redirect()

({  }) => ("/new")          // 302
({  }) => redirect("/new", 301)     // or 303, 307, 308

Sends the client elsewhere. A location with characters a header cannot carry is percent-encoded first.

notFound()

({ ,  }) => findTea(.id) ?? (`No tea ${.id}`)

Ends the request with a 404 problem, the same one an unknown route gets, through onProblem like any other. To end with another problem, throw one: throw problem(409, "taken", "That name is taken").

setCookie(), clearCookie()

({  }) => {
  ("sid", session.id, { : 60 * 60 * 24, : true });
  return { : true };
};
({  }) => ("sid");

A cookie on the answer; every cookie goes out as a Set-Cookie of its own.

optiondefault
maxAge, expiresuntil the browser closesseconds, or a date; maxAge: 0 deletes it
path/
domain
securefalseonly over HTTPS; turn it on behind TLS
httpOnlytrueout of reach of the page's scripts
sameSite"Lax""Strict", "Lax" or "None", which needs secure
partitioned

csv()

.("/export", ({ ,  }) =>
  (rowsFromDb(), { : "teas.csv", : ";", : true }),
);

Rows as a CSV file, written as they come: an array, or a generator over a database cursor, so a million rows never sit in memory at once.

optiondefault
columnsthe keys of the first rowa list of keys, or key → heading
separator,; for a German Excel
bomfalsetrue so Excel reads the file as UTF-8
filenameoffered for download under this name
guardFormulastruea cell that starts with =, +, - or @ gets a ', so a spreadsheet shows it instead of running it

Decorations

.("db", openDatabase());
.("/teas", ({  }) => .teas.all());

What decorate() puts on the context of a scope is there too, typed.

On this page