Set Cache-Control on every rendered page, and record the CSRF token's unbinding #178
@@ -221,6 +221,9 @@ Revisit only if the stated reason stops holding.
|
|||||||
adds you to any group, including one holding every permission; `users:write` mints a recovery code
|
adds you to any group, including one holding every permission; `users:write` mints a recovery code
|
||||||
for any account. The containment the split buys is real on the **read** half only (`users:read` is
|
for any account. The containment the split buys is real on the **read** half only (`users:read` is
|
||||||
a safe helpdesk grant). Don't let the per-resource naming imply otherwise in docs.
|
a safe helpdesk grant). Don't let the per-resource naming imply otherwise in docs.
|
||||||
|
- **The CSRF token is deliberately unbound to the session** (`src/auth/csrf.ts`; README → Security
|
||||||
|
model): the usual cost of a signed double-submit token, covered by `SameSite=Lax` and HSTS.
|
||||||
|
**Valid while the session cookie stays `SameSite=Lax` or stricter.**
|
||||||
- **Plainpages says "user" everywhere; Ory's word is "identity".** House style, not a renamed
|
- **Plainpages says "user" everywhere; Ory's word is "identity".** House style, not a renamed
|
||||||
concept. The single exception is the `Identity` DTO in `src/auth/kratos-admin.ts`, which mirrors
|
concept. The single exception is the `Identity` DTO in `src/auth/kratos-admin.ts`, which mirrors
|
||||||
Kratos' wire shape — don't rename it.
|
Kratos' wire shape — don't rename it.
|
||||||
|
|||||||
@@ -12,6 +12,13 @@ The topbar, filter bar, pager, form page and empty-table message inset their con
|
|||||||
(12px), as table cells do, instead of 16px. A plugin that overrides `--pad-x` now moves all of them
|
(12px), as table cells do, instead of 16px. A plugin that overrides `--pad-x` now moves all of them
|
||||||
together.
|
together.
|
||||||
|
|
||||||
|
### Pages are not kept by a shared cache
|
||||||
|
|
||||||
|
Every page and JSON response now carries `Cache-Control: private, no-store`, so a cache or reverse
|
||||||
|
proxy in front of Plainpages can't store a signed-in page and serve it to the next visitor. A route
|
||||||
|
whose page is the same for everyone can still set its own `cache-control` through
|
||||||
|
`RouteResult.headers`; assets are unchanged.
|
||||||
|
|
||||||
## 0.4.0
|
## 0.4.0
|
||||||
|
|
||||||
**Breaking.** Set `apiVersion: "0.4.0"`. The app shell no longer bounds the content column, so a page
|
**Breaking.** Set `apiVersion: "0.4.0"`. The app shell no longer bounds the content column, so a page
|
||||||
|
|||||||
@@ -1384,6 +1384,11 @@ authorization graph, so treat it the same. `compose.yml` publishes none of the s
|
|||||||
so a stolen cookie jar is worth 30 days of re-mintable access, not ten minutes. Only our two cookies
|
so a stolen cookie jar is worth 30 days of re-mintable access, not ten minutes. Only our two cookies
|
||||||
obey `SECURE_COOKIES`; the Kratos one takes its flags from Kratos' own config.
|
obey `SECURE_COOKIES`; the Kratos one takes its flags from Kratos' own config.
|
||||||
|
|
||||||
|
**The CSRF token isn't bound to the session.** It is a signed double-submit token, so an attacker who
|
||||||
|
can write cookies on your origin can plant one they know. `SameSite=Lax` keeps a cross-site page
|
||||||
|
out, and HSTS (`SECURE_COOKIES=true`) a network attacker. A hostile page on a sibling subdomain can
|
||||||
|
still set cookies, so host nothing untrusted on one.
|
||||||
|
|
||||||
**Offboarding is not instant by default** — a revoked permission or deactivated identity lands
|
**Offboarding is not instant by default** — a revoked permission or deactivated identity lands
|
||||||
within one token TTL, unless the [denylist](#instant-revoke-the-optional-denylist) is on.
|
within one token TTL, unless the [denylist](#instant-revoke-the-optional-denylist) is on.
|
||||||
|
|
||||||
@@ -1645,6 +1650,11 @@ Every response carries security headers (`src/http/security-headers.ts`): a stri
|
|||||||
so a branding logo must live under `/public/` or be a `data:` URI; a plugin route can override any
|
so a branding logo must live under `/public/` or be a `data:` URI; a plugin route can override any
|
||||||
header per-response via `RouteResult.headers`.
|
header per-response via `RouteResult.headers`.
|
||||||
|
|
||||||
|
Every page and JSON response is `Cache-Control: private, no-store`, so a cache in front of `web`
|
||||||
|
never keeps one user's page for the next. A route may override it only for a page that is the same for
|
||||||
|
every visitor: no user, no CSRF token, no `Set-Cookie`, since the host adds a fresh CSRF cookie to
|
||||||
|
every plugin route.
|
||||||
|
|
||||||
A deep link reached while signed out — or after the session JWT lapses mid-task — bounces to the
|
A deep link reached while signed out — or after the session JWT lapses mid-task — bounces to the
|
||||||
themed sign-in and returns to the **page that was requested** (`return_to`, validated host-relative
|
themed sign-in and returns to the **page that was requested** (`return_to`, validated host-relative
|
||||||
by `localPath` in `src/http/safe-url.ts`, so a crafted value can't turn login completion into an open
|
by `localPath` in `src/http/safe-url.ts`, so a crafted value can't turn login completion into an open
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export default defineConfig({
|
|||||||
testDir: ".",
|
testDir: ".",
|
||||||
outputDir: "artifacts/test-output",
|
outputDir: "artifacts/test-output",
|
||||||
fullyParallel: true,
|
fullyParallel: true,
|
||||||
|
workers: 4, // Playwright's default is half the cores, so a run's parallelism would depend on the machine
|
||||||
forbidOnly: true,
|
forbidOnly: true,
|
||||||
reporter: [["list"], ["html", { open: "never", outputFolder: "artifacts/report" }]],
|
reporter: [["list"], ["html", { open: "never", outputFolder: "artifacts/report" }]],
|
||||||
use: {
|
use: {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
Scheduling · Shifts list (reference plugin). The handler fetched the rows from the upstream
|
Scheduling · Shifts list (reference plugin). The handler fetched the rows from the upstream
|
||||||
service; this view renders them with the core building blocks inside the native app shell
|
service; this view renders them with the core building blocks inside the native app shell
|
||||||
(ctx.chrome). `include()` reaches the core partials (shell, nav-tree, filter-bar, data-table,
|
(ctx.chrome). `include()` reaches the core partials (shell, nav-tree, filter-bar, data-table,
|
||||||
alert) — see docs/plugin-contract.md. Zero-JS: search round-trips the URL.
|
alert) — see the README. Zero-JS: search round-trips the URL.
|
||||||
Data: chrome, title, breadcrumbs, count, filterBar, table, canWrite, newHref, error?
|
Data: chrome, title, breadcrumbs, count, filterBar, table, canWrite, newHref, error?
|
||||||
%><%
|
%><%
|
||||||
const navHtml = include("partials/nav-tree", { nodes: chrome.nav });
|
const navHtml = include("partials/nav-tree", { nodes: chrome.nav });
|
||||||
|
|||||||
@@ -284,6 +284,39 @@ test("every response carries the security headers; HSTS follows SECURE_COOKIES",
|
|||||||
assert.match(res.headers.get("strict-transport-security") ?? "", /max-age=\d+/);
|
assert.match(res.headers.get("strict-transport-security") ?? "", /max-age=\d+/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Cache-Control: pages and JSON are private, no-store unless the route sets its own; assets are left alone", async (t) => {
|
||||||
|
const NO_STORE = "private, no-store";
|
||||||
|
const csrf = (await fetch(base + "/")).headers.get("set-cookie")?.split(";")[0] ?? "";
|
||||||
|
assert.match(csrf, new RegExp(`^${CSRF_COOKIE}=`));
|
||||||
|
|
||||||
|
// The signed-out landing is no-store as well: it and the signed-in page share one URL.
|
||||||
|
assert.equal((await fetch(base + "/")).headers.get("cache-control"), NO_STORE);
|
||||||
|
assert.equal((await fetch(base + "/", { headers: { cookie: csrf } })).headers.get("cache-control"), NO_STORE);
|
||||||
|
assert.equal((await fetch(base + "/", { headers: { cookie: csrf }, method: "HEAD" })).headers.get("cache-control"), NO_STORE);
|
||||||
|
assert.equal((await fetch(base + "/", { headers: { cookie: `${csrf}; ${session()}` } })).headers.get("cache-control"), NO_STORE);
|
||||||
|
|
||||||
|
assert.equal((await fetch(base + "/dashboard", { headers: { cookie: session() } })).headers.get("cache-control"), NO_STORE);
|
||||||
|
assert.equal((await fetch(base + "/dashboard", { redirect: "manual" })).headers.get("cache-control"), NO_STORE); // sign-in bounce
|
||||||
|
assert.equal((await fetch(base + "/nope", { headers: { cookie: csrf } })).headers.get("cache-control"), NO_STORE); // 404
|
||||||
|
assert.equal((await fetch(base + "/public/css/styles.css")).headers.get("cache-control"), null);
|
||||||
|
|
||||||
|
const demo: Plugin = {
|
||||||
|
apiVersion: "1.0.0",
|
||||||
|
home: () => ({ html: "<p>plugin home</p>" }),
|
||||||
|
id: "demo",
|
||||||
|
routes: [
|
||||||
|
{ handler: () => ({ html: "open" }), method: "GET", path: "/open", public: true },
|
||||||
|
{ handler: () => ({ headers: { "Cache-Control": "public, max-age=600" }, html: "same" }), method: "GET", path: "/same", public: true },
|
||||||
|
{ handler: () => ({ json: { ok: true } }), method: "GET", path: "/data", public: true },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const url = await startApp(t, [demo]);
|
||||||
|
for (const path of ["/demo/open", "/demo/data", "/"]) {
|
||||||
|
assert.equal((await fetch(url + path)).headers.get("cache-control"), NO_STORE, path);
|
||||||
|
}
|
||||||
|
assert.equal((await fetch(url + "/demo/same")).headers.get("cache-control"), "public, max-age=600");
|
||||||
|
});
|
||||||
|
|
||||||
// Production caches compiled templates; rendering must stay correct across repeated requests.
|
// Production caches compiled templates; rendering must stay correct across repeated requests.
|
||||||
test("renders correctly with template caching enabled", async () => {
|
test("renders correctly with template caching enabled", async () => {
|
||||||
const app = createApp({ cache: true });
|
const app = createApp({ cache: true });
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ import { localPath } from "./safe-url.ts";
|
|||||||
import { routePublic, serveStatic } from "./static.ts";
|
import { routePublic, serveStatic } from "./static.ts";
|
||||||
import { renderPluginView } from "../plugin-host/view-resolver.ts";
|
import { renderPluginView } from "../plugin-host/view-resolver.ts";
|
||||||
|
|
||||||
|
const NO_STORE = "private, no-store";
|
||||||
|
|
||||||
const rootDir = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
const rootDir = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||||
|
|
||||||
export interface AppOptions {
|
export interface AppOptions {
|
||||||
@@ -203,6 +205,11 @@ export function createApp(options: AppOptions = {}): Server {
|
|||||||
// A cache in front of us must key on the language. Set after the static branch: an asset is
|
// A cache in front of us must key on the language. Set after the static branch: an asset is
|
||||||
// the same bytes in every language, and a Vary there fragments its entry per raw header.
|
// the same bytes in every language, and a Vary there fragments its entry per raw header.
|
||||||
res.setHeader("vary", "accept-language");
|
res.setHeader("vary", "accept-language");
|
||||||
|
// Fail closed: a cache in front of us must not keep a page that may carry a user's data. Not
|
||||||
|
// even the signed-out `/` is shared: it and the signed-in page have one URL, and a cache keyed
|
||||||
|
// on URL and Accept-Language would hand the signed-out one to a signed-in visitor. A route that
|
||||||
|
// is the same for everyone sets its own value in RouteResult.headers; the host doesn't police it.
|
||||||
|
res.setHeader("cache-control", NO_STORE);
|
||||||
|
|
||||||
// Canonical host (APP_URL): send an off-host visitor to the configured origin so the browser,
|
// Canonical host (APP_URL): send an off-host visitor to the configured origin so the browser,
|
||||||
// the themed forms and the cross-origin Kratos POST share one cookie host — otherwise the
|
// the themed forms and the cross-origin Kratos POST share one cookie host — otherwise the
|
||||||
|
|||||||
@@ -6,13 +6,12 @@
|
|||||||
|
|
||||||
`Bar = 1`
|
`Bar = 1`
|
||||||
|
|
||||||
`Next ID = 33`
|
`Next ID = 34`
|
||||||
|
|
||||||
## Items
|
## Items
|
||||||
|
|
||||||
| ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score |
|
| ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score |
|
||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
| 15 | 0.4.21 | | security | **Set `Cache-Control` on every rendered page from the host: `private, no-store` on a gated or signed-in page, a cacheable value on a public one.** | 2 | 2 | 2 | 4 | 3 | 8 by 3 | 3.0 |
|
|
||||||
| 30 | 0.4.21 | | defect | **Give every `oklch()` colour a fallback, so primary buttons stay readable on an engine without it.** | 1 | 2 | 2 | 3 | 2 | 4 by 5 | 29.9 |
|
| 30 | 0.4.21 | | defect | **Give every `oklch()` colour a fallback, so primary buttons stay readable on an engine without it.** | 1 | 2 | 2 | 3 | 2 | 4 by 5 | 29.9 |
|
||||||
| 29 | 0.4.21 | | defect | **Show the whole nav tree on an engine without `:has()`, and fold it only where `:has()` works.** | 2 | 2 | 2 | 4 | 3 | 4 by 6 | 26.9 |
|
| 29 | 0.4.21 | | defect | **Show the whole nav tree on an engine without `:has()`, and fold it only where `:has()` works.** | 2 | 2 | 2 | 4 | 3 | 4 by 6 | 26.9 |
|
||||||
| 31 | 0.4.21 | | | **Delete the design-preview rules in `public/css/auth.css` that no view uses.** | 1 | 1 | 1 | 1 | 2 | code health by 2, 8 by 1 | 13.3 |
|
| 31 | 0.4.21 | | | **Delete the design-preview rules in `public/css/auth.css` that no view uses.** | 1 | 1 | 1 | 1 | 2 | code health by 2, 8 by 1 | 13.3 |
|
||||||
@@ -21,12 +20,9 @@
|
|||||||
| 9 | 0.4.21 | | | **Typecheck `e2e-tests/` in the gate.** | 1 | 1 | 1 | 1 | 3 | code health by 2 | 9.0 |
|
| 9 | 0.4.21 | | | **Typecheck `e2e-tests/` in the gate.** | 1 | 1 | 1 | 1 | 3 | code health by 2 | 9.0 |
|
||||||
| 24 | 0.4.21 | | | **Retire `src/ui/shell-context.ts` and render the dashboard from `ctx.chrome`.** | 2 | 1 | 2 | 3 | 4 | code health by 3 | 9.0 |
|
| 24 | 0.4.21 | | | **Retire `src/ui/shell-context.ts` and render the dashboard from `ctx.chrome`.** | 2 | 1 | 2 | 3 | 4 | code health by 3 | 9.0 |
|
||||||
| 17 | 0.4.21 | | | **Remove the icons no caller uses from `ICON_NAMES` and the sprite.** | 1 | 1 | 1 | 1 | 1 | 8 by 1, code health by 1 | 8.8 |
|
| 17 | 0.4.21 | | | **Remove the icons no caller uses from `ICON_NAMES` and the sprite.** | 1 | 1 | 1 | 1 | 1 | 8 by 1, code health by 1 | 8.8 |
|
||||||
| 10 | 0.4.21 | | | **Pin Playwright's `workers` to a fixed number.** | 1 | 1 | 1 | 1 | 1 | code health by 1 | 7.2 |
|
|
||||||
| 18 | 0.4.21 | | | **Record in AGENTS.md and README → Security model that the CSRF token is deliberately unbound to the session, and why.** | 1 | 1 | 1 | 1 | 1 | code health by 1 | 7.2 |
|
|
||||||
| 21 | 0.4.21 | | | **Fail the gate on a function that exceeds a size or complexity limit.** | 3 | 1 | 1 | 3 | 5 | code health by 4 | 7.2 |
|
| 21 | 0.4.21 | | | **Fail the gate on a function that exceeds a size or complexity limit.** | 3 | 1 | 1 | 3 | 5 | code health by 4 | 7.2 |
|
||||||
| 32 | 0.4.21 | 29, 30 | | **Fail the gate on a stylesheet feature the browser floor lacks, unless it is guarded.** | 3 | 1 | 1 | 3 | 4 | code health by 2 | 6.0 |
|
| 32 | 0.4.21 | 29, 30 | | **Fail the gate on a stylesheet feature the browser floor lacks, unless it is guarded.** | 3 | 1 | 1 | 3 | 4 | code health by 2 | 6.0 |
|
||||||
| 27 | 0.4.21 | | | **Inset the scheduling example's lead text, shift count, alerts and call-to-action by `--pad-x`, like the bars around them.** | 1 | 2 | 1 | 2 | 1 | 9 by 2, 2 by 1 | 4.8 |
|
| 27 | 0.4.21 | | | **Inset the scheduling example's lead text, shift count, alerts and call-to-action by `--pad-x`, like the bars around them.** | 1 | 2 | 1 | 2 | 1 | 9 by 2, 2 by 1 | 4.8 |
|
||||||
| 25 | 0.4.21 | | | **Point the views' references to `docs/plugin-contract.md` at the README.** | 1 | 2 | 1 | 2 | 1 | 9 by 3 | 4.6 |
|
|
||||||
| 20 | 0.4.21 | | | **Redirect an anonymous hit on a session-needing flow page to sign-in, instead of a 500.** | 2 | 2 | 2 | 4 | 3 | 2 by 4 | 4.2 |
|
| 20 | 0.4.21 | | | **Redirect an anonymous hit on a session-needing flow page to sign-in, instead of a 500.** | 2 | 2 | 2 | 4 | 3 | 2 by 4 | 4.2 |
|
||||||
| 4 | 0.4.21 | | | **Refuse a permission save whose baseline no longer matches Keto, so concurrent edits are not silently lost.** | 2 | 2 | 2 | 4 | 4 | 2 by 4 | 2.8 |
|
| 4 | 0.4.21 | | | **Refuse a permission save whose baseline no longer matches Keto, so concurrent edits are not silently lost.** | 2 | 2 | 2 | 4 | 4 | 2 by 4 | 2.8 |
|
||||||
| 7 | 0.4.21 | | | **Confirm a successful permission save, and report a partial failure instead of hiding it.** | 2 | 2 | 2 | 4 | 4 | 2 by 4 | 2.8 |
|
| 7 | 0.4.21 | | | **Confirm a successful permission save, and report a partial failure instead of hiding it.** | 2 | 2 | 2 | 4 | 4 | 2 by 4 | 2.8 |
|
||||||
@@ -35,6 +31,7 @@
|
|||||||
| 1 | 0.4.21 | | owner | **Publish every release to Docker Hub, and give the Hub page its short description.** | 3 | 3 | 2 | 6 | 5 | 1 by 3 | 1.3 |
|
| 1 | 0.4.21 | | owner | **Publish every release to Docker Hub, and give the Hub page its short description.** | 3 | 3 | 2 | 6 | 5 | 1 by 3 | 1.3 |
|
||||||
| 13 | 0.4.21 | | | **Accept a verification code pasted with surrounding whitespace.** | 4 | 2 | 3 | 7 | 7 | 2 by 8 | 1.0 |
|
| 13 | 0.4.21 | | | **Accept a verification code pasted with surrounding whitespace.** | 4 | 2 | 3 | 7 | 7 | 2 by 8 | 1.0 |
|
||||||
| 19 | 0.5.0 | | question | **Run the documented Docker commands on macOS and fix whatever misbehaves.** | 2 | 2 | 1 | 3 | 3 | 1 by 3 | 3.3 |
|
| 19 | 0.5.0 | | question | **Run the documented Docker commands on macOS and fix whatever misbehaves.** | 2 | 2 | 1 | 3 | 3 | 1 by 3 | 3.3 |
|
||||||
|
| 33 | 0.5.0 | | question | **Decide whether any public page gets a shared-cache `Cache-Control`, now that the host sends `private, no-store` on every page.** | 2 | 3 | 4 | 7 | 3 | 8 by 2 | 2.0 |
|
||||||
| 16 | 0.5.0 | | question | **Decide whether the single Keto `Resource` namespace splits into per-domain namespaces.** | 3 | 2 | 3 | 6 | 6 | 7 by 2 | 0.5 |
|
| 16 | 0.5.0 | | question | **Decide whether the single Keto `Resource` namespace splits into per-domain namespaces.** | 3 | 2 | 3 | 6 | 6 | 7 by 2 | 0.5 |
|
||||||
| 6 | 0.5.0 | | | **Warn at discovery when a route or nav node gates on a permission no plugin declares.** | 2 | 1 | 1 | 2 | 2 | 7 by 3, 9 by 3 | 8.0 |
|
| 6 | 0.5.0 | | | **Warn at discovery when a route or nav node gates on a permission no plugin declares.** | 2 | 1 | 1 | 2 | 2 | 7 by 3, 9 by 3 | 8.0 |
|
||||||
| 23 | 0.5.0 | | | **Give list pages one view-model helper from query to pagination, instead of ~40 hand-written lines each.** | 3 | 1 | 2 | 4 | 5 | 1 by 4, 9 by 2, code health by 3 | 7.9 |
|
| 23 | 0.5.0 | | | **Give list pages one view-model helper from query to pagination, instead of ~40 hand-written lines each.** | 3 | 1 | 2 | 4 | 5 | 1 by 4, 9 by 2, code health by 3 | 7.9 |
|
||||||
@@ -45,12 +42,6 @@
|
|||||||
|
|
||||||
## Details
|
## Details
|
||||||
|
|
||||||
### 15. Set `Cache-Control` on every rendered page from the host: `private, no-store` on a gated or signed-in page, a cacheable value on a public one.
|
|
||||||
|
|
||||||
The host owns it, not the reverse proxy. Today responses carry `Vary: Accept-Language` but no
|
|
||||||
`Cache-Control`, so a shared cache in front of `web` may store a signed-in page, with that user's
|
|
||||||
data, and serve it to the next visitor.
|
|
||||||
|
|
||||||
### 30. Give every `oklch()` colour a fallback, so primary buttons stay readable on an engine without it.
|
### 30. Give every `oklch()` colour a fallback, so primary buttons stay readable on an engine without it.
|
||||||
|
|
||||||
The accent, focus and status tokens in `public/css/styles.css` are `oklch()`, which needs Chromium
|
The accent, focus and status tokens in `public/css/styles.css` are `oklch()`, which needs Chromium
|
||||||
@@ -85,13 +76,6 @@ almost every field. Keep `shellUser` as the shared primitive.
|
|||||||
`i-chart`, `i-copy` and `i-download` have no caller, and the sprite is inlined into every page.
|
`i-chart`, `i-copy` and `i-download` have no caller, and the sprite is inlined into every page.
|
||||||
AGENTS.md → "`ICON_NAMES` is a host-owned registry" already makes this routine cleanup.
|
AGENTS.md → "`ICON_NAMES` is a host-owned registry" already makes this routine cleanup.
|
||||||
|
|
||||||
### 18. Record in AGENTS.md and README → Security model that the CSRF token is deliberately unbound to the session, and why.
|
|
||||||
|
|
||||||
`src/auth/csrf.ts` signs `<nonce>.<HMAC(secret, nonce)>` with no session binding, so an attacker who
|
|
||||||
can write cookies on the origin can fix a token they know. Accepted: it is standard for unbound
|
|
||||||
signed double-submit, and `SameSite=Lax` plus HSTS cover it. Valid while the session cookie stays
|
|
||||||
`SameSite=Lax` or stricter.
|
|
||||||
|
|
||||||
### 21. Fail the gate on a function that exceeds a size or complexity limit.
|
### 21. Fail the gate on a function that exceeds a size or complexity limit.
|
||||||
|
|
||||||
Only `tsc --strict` runs today. A new tool is a new dependency, so justify it.
|
Only `tsc --strict` runs today. A new tool is a new dependency, so justify it.
|
||||||
@@ -107,10 +91,6 @@ allow a feature only inside an `@supports` that guards it.
|
|||||||
They sit flush against the content pane's edge while the filter bar, table and pager sit 12px in.
|
They sit flush against the content pane's edge while the filter bar, table and pager sit 12px in.
|
||||||
The fix belongs in `examples/plugins/scheduling/public/scheduling.css`; check each page in a browser.
|
The fix belongs in `examples/plugins/scheduling/public/scheduling.css`; check each page in a browser.
|
||||||
|
|
||||||
### 25. Point the views' references to `docs/plugin-contract.md` at the README.
|
|
||||||
|
|
||||||
`examples/plugins/scheduling/views/shifts.ejs` still cites it.
|
|
||||||
|
|
||||||
### 20. Redirect an anonymous hit on a session-needing flow page to sign-in, instead of a 500.
|
### 20. Redirect an anonymous hit on a session-needing flow page to sign-in, instead of a 500.
|
||||||
|
|
||||||
`flowPage` (`src/auth/routes.ts`) rethrows a 401, which Kratos answers to a settings-flow init with no
|
`flowPage` (`src/auth/routes.ts`) rethrows a 401, which Kratos answers to a settings-flow init with no
|
||||||
@@ -169,6 +149,15 @@ test covers. Code entry has no E2E coverage today.
|
|||||||
Needs a Mac. Two suspects, both from `--user "$(id -u):$(id -g)"`: a macOS `id -g` is `20`, which is
|
Needs a Mac. Two suspects, both from `--user "$(id -u):$(id -g)"`: a macOS `id -g` is `20`, which is
|
||||||
`dialout` inside the noble image, and Docker Desktop remaps bind-mount ownership in its VM.
|
`dialout` inside the noble image, and Docker Desktop remaps bind-mount ownership in its VM.
|
||||||
|
|
||||||
|
### 33. Decide whether any public page gets a shared-cache `Cache-Control`, now that the host sends `private, no-store` on every page.
|
||||||
|
|
||||||
|
Every non-asset response is `private, no-store`. The built-in signed-out `/` and its signed-in
|
||||||
|
variant share one URL, so a shared cache keyed on URL and `Accept-Language` would serve the
|
||||||
|
signed-out page to a signed-in visitor for the cache lifetime, and a cache that bypasses on cookies
|
||||||
|
would never store it. Options: leave it, so operators cache only assets; add `Vary: Cookie`; or give
|
||||||
|
the signed-out page its own URL. A plugin route can already opt in through `RouteResult.headers`.
|
||||||
|
I is 4 because a wrong rule caches a signed-in page for everyone, exposing PII.
|
||||||
|
|
||||||
### 16. Decide whether the single Keto `Resource` namespace splits into per-domain namespaces.
|
### 16. Decide whether the single Keto `Resource` namespace splits into per-domain namespaces.
|
||||||
|
|
||||||
Ory's own examples model `Shift`, `Document` and so on. One global `Resource` bucket is the
|
Ory's own examples model `Shift`, `Document` and so on. One global `Resource` bucket is the
|
||||||
|
|||||||
Reference in New Issue
Block a user