Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion public/skills/icons/references/logos.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

Use `@askrjs/logos` for an authorized brand mark, not for general interface
concepts. The installed package currently determines which marks are available.
At version 0.2.0 it exports Apple, Facebook, GitHub, Google, and Microsoft logo
At version 0.2.1 it exports Apple, Facebook, GitHub, Google, and Microsoft logo
components. Verify the installed declarations rather than assuming another
brand is present.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,16 @@ src/
components/
project-card.tsx
main.tsx
server.ts
server.ts
```

Keep a small application smaller. Retain `pages/_routes.tsx` and
`pages/_layout.tsx` as the readable root of a browser application's route tree.
Add a nested page group only when it needs its own route prefix, layout, access
boundary, or navigation context.

The browser entry owns mounting or hydration. A server entry owns server
The browser entry (`src/main.tsx`) owns mounting or hydration. The server
entry (`server.ts`, at the project root beside `package.json`) owns server
composition. Neither should become a dumping ground for feature behavior.

## Keep pages thin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,20 @@

## Render the state contract

Read the discriminated fields directly:
A query exposes `data`, `error`, the booleans `loading`, `refreshing`, and
`stale`, plus a `consistency` field and a `staleReason`. Read them directly
rather than inferring semantics from another query library:

- `loading` means the first request has not produced data; `data` is `null`.
- `fresh` means committed data is current.
- `refreshing` keeps previous data while a confirming fetch runs.
- `pending-write` keeps previous data while a successful write is being
confirmed.
- `stale` is settled but not current, with reason `inconsistent`, `aborted`, or
`error`.
- `consistency === 'fresh'` means committed data is current.
- `refreshing` (and `consistency === 'refreshing'`) keeps previous data while
a confirming fetch runs.
- `consistency === 'pending-write'` keeps previous data while a successful
write is being confirmed.
- `stale` means the query is not fresh; it can also be `true` while refreshing
or confirming a pending write, when `staleReason` is `null`.
- `consistency === 'stale'` is settled but not current, with `staleReason` of
`inconsistent`, `aborted`, or `error`.

Use `refresh()` for explicit retry or user refresh. Concurrent manual refresh
calls coalesce while work is pending.
Expand Down
9 changes: 9 additions & 0 deletions src/pages/docs/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,10 @@ const rendering = sectionPages('Rendering', 'Rendering', 'rendering', [
},
{
title: 'Selective Hydration',
// @askrjs/askr/boot only exports createIsland/createIslands/createSPA/
// hydrateSPA — there's no boundary-selection or priority API to hydrate
// part of a page before the rest. This page describes the pattern you'd
// want, not something the runtime ships today.
status: 'limited',
headings: [
'Boundary selection',
Expand Down Expand Up @@ -1065,6 +1069,11 @@ const overlays = componentPages('Overlays', [
},
{
title: 'Drawer and Sheet',
// Not primitive-less like the experimentalControls set above — both are
// straight aliases of @askrjs/ui/dialog (askr-themes/src/entries/
// {drawer,sheet}.ts). Sheet gets real data-side positioning CSS; Drawer
// has none at all — neither has distinct gesture or animation behavior
// of its own beyond what Dialog already provides.
status: 'experimental',
ui: [],
themes: ['drawer', 'sheet'],
Expand Down
9 changes: 9 additions & 0 deletions src/pages/docs/component-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ export function componentPropReferences(
])
);
members.delete('maxWidth');
// ContainerProps is `Omit<BlockDivProps, "maxWidth">`, and BlockDivProps
// fixes `as` to the literal "div" (intersecting BlockOwnProps's generic
// `as?: BlockElement` with BlockElementProps<"div">'s `as?: "div"`).
// Inheriting BlockOwnProps's own `as` member here would wrongly imply
// Container accepts any BlockElement tag, so narrow it back down.
const asMember = members.get('as');
if (asMember) {
members.set('as', { ...asMember, signature: 'as?: "div" | undefined;' });
}
byName.set('ContainerProps', {
...container,
members: [...members.values()].sort((left, right) =>
Expand Down
8 changes: 4 additions & 4 deletions src/pages/docs/content-overrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const headingOverrides: Readonly<
'browser-and-api-clients':
"Because AuthOptions can hold both a sessionCookie-based session lookup and a jwt or jwtCookie validator at the same time, one createAuth() call can serve cookie-carrying browsers and bearer-token API clients without standing up two separate resolvers. @askrjs/server exposes the resolved value as ctx.auth on every request, so route code written against ctx.auth.principal doesn't need to know which path produced it. That's also what lets the same requirePermission()/requireUser() checks apply uniformly to both kinds of caller.",
'failure-behavior':
"A lookup that fails to identify anyone resolves to authenticated: false with principal and session set to null rather than throwing, so handlers can inspect ctx.auth without try/catch. Turning that into an actual accept/reject decision is the job of the requirement functions covered under Authorization Requirements, which map it to an AuthDecision of {allowed: false, reason: 'unauthenticated' | 'forbidden' | 'already_authenticated'}. A malformed bearer-header JWT does throw, surfacing as a JwtValidationError from the JWT validator — but a malformed JWT cookie is caught internally and resolution falls through to an anonymous context instead, so the two malformed-token paths don't behave the same way.",
"A lookup that fails to identify anyone resolves to authenticated: false with principal and session set to null rather than throwing, so handlers can inspect ctx.auth without try/catch. Turning that into an actual accept/reject decision is the job of the requirement functions covered under Authorization Requirements, which map it to an AuthDecision of {allowed: false, reason: 'unauthenticated' | 'forbidden' | 'already_authenticated'}. A malformed bearer-header JWT and a malformed JWT cookie behave the same way: both go through the JWT validator, and a JwtValidationError from either path is caught internally, so resolution falls through to an anonymous context instead of throwing out to your route.",
'identity-model':
"A Principal (id, subject?, roles?, permissions?) and an AuthSession (id, subject, expiresAt?, revokedAt?) are separate types in @askrjs/auth, both extending a Claim base of Record<string, unknown> so you can attach whatever custom fields your provider sends. createAuth(options) builds an AuthResolver whose resolve(request) returns one AuthContext per request, bundling authenticated, principal, session, tenant, and an optional scopes list into a single object. Route handlers and requirement functions both read from that same AuthContext, so there's one shape to reason about regardless of how the principal was established.",
'session-boundary':
Expand Down Expand Up @@ -1402,7 +1402,7 @@ export const headingOverrides: Readonly<
},
'/docs/data/queries-and-consistency': {
'consistency-modes':
"Instead of a single isLoading flag, a Query<T>'s consistency field is one of 'fresh', 'stale', 'refreshing', or 'pending-write', and each value comes with a matching shape for data, error, and stale flags — a refreshing query still has its previous data available, for instance, while a stale query with an error has both data: null and staleReason: 'error'. Reading consistency first tells you which of those shapes you're dealing with before you touch data or error directly.",
"Instead of a single isLoading flag, a Query<T>'s consistency field is one of 'fresh', 'stale', 'refreshing', or 'pending-write', and each value comes with a matching shape for data, error, and stale flags — a refreshing query still has its previous data available, for instance, while a stale query with an error has staleReason: 'error' and either retains previous data or has data: null when no successful value exists. Reading consistency first tells you which of those shapes you're dealing with before you touch data or error directly.",
'define-a-query':
"defineQuery() takes a QueryDefinition — a key(input) function that derives a cache key, and a fetch(context) function that does the actual request — and hands back a reusable definition you can pass to createQuery(), serveQuery(), or prefetchQuery(). createQuery() also accepts a plain QueryOptions object directly if you don't need the input/key(input) split. Optional isConsistent() and reconcile() functions on the definition don't gate whether fetched data reaches the cache — freshly fetched data is written to the query's state either way. What they control is what happens next: isConsistent() returning false marks the result stale (consistency: 'stale', staleReason: 'inconsistent') and triggers reconcile(), whose return value decides whether the query automatically retries.",
'query-scopes':
Expand Down Expand Up @@ -1553,7 +1553,7 @@ export const headingOverrides: Readonly<
implementation:
"On the server, attach `auth: requirePermission('reports:read')` to individual routes and read `ctx.auth.principal` in the handler; `createJwtValidator()` or a JWKS-backed validator plugs into `AuthOptions.jwt` if you're validating bearer tokens rather than cookies. On the client, `route(path, Component, { policies: [...] })` — plural, an array of requirement functions, not a single `policy` field — with `allow()`/`deny()`/`redirect()` decisions from your policy functions keeps navigation from ever rendering a page the user shouldn't see, and `currentAuth()` gives components access to the resolved identity for the route currently rendering.",
verification:
'Test all three identity states explicitly: anonymous, authenticated without the required role, and authenticated with it — each should reach a different, correct outcome on both server routes and client-rendered pages. Confirm a token that fails `JwtValidator` verification is rejected with a clear error, not silently treated as anonymous. Finally verify that server-side auth decisions and client-side route policies agree; a route hidden in the nav but reachable directly by URL because only the client checked permissions is a real vulnerability, not just a UX gap.',
'Test all three identity states explicitly: anonymous, authenticated without the required role, and authenticated with it — each should reach a different, correct outcome on both server routes and client-rendered pages. Confirm a token that fails `JwtValidator` verification resolves to an unauthenticated context and is then rejected by the route requirement with a clear 401 or redirect, without leaking validation details. Finally verify that server-side auth decisions and client-side route policies agree; a route hidden in the nav but reachable directly by URL because only the client checked permissions is a real vulnerability, not just a UX gap.',
},
'/docs/guides/build-an-mcp-server': {
'failure-states':
Expand Down Expand Up @@ -2181,7 +2181,7 @@ export const headingOverrides: Readonly<
'context-first-handlers':
'Every handler and every Middleware receives a single ServerContext argument instead of separate request/response objects — request, url, params, headers, query, state, auth, and an AbortSignal all live on ctx. Dependencies like a database client are deliberately not part of that context; the README shows creating them at your composition root and passing them into route registration functions, keeping ctx reserved for request-scoped data. Because the context also carries every response helper (ctx.ok, ctx.notFound, and so on), a handler can build its whole response without importing anything else.',
'production-boundary':
"ServerAppOptions accepts an onError(error, context) hook that runs for an unhandled exception a handler or middleware throws, so you control what most errors turn into instead of leaking a stack trace to the client — but it's the fallback, not the only path: a few framework-recognized failures (an oversized request body, a malformed path parameter, a ctx.bind() failure) are converted to a fixed problem+json response before onError is ever consulted, so don't rely on it to customize those specific cases. The OpenAPI layer defaults to validateResponses: false and only turns response validation on when you explicitly opt in outside production, because checking every response against its schema costs real time on every request. The package is published in the 0.0.x line, so pin versions and review release notes before moving a production app between versions.",
"ServerAppOptions accepts an onError(error, context) hook that runs for an unhandled exception a handler or middleware throws, so you control what most errors turn into instead of leaking a stack trace to the client — but it's the fallback, not the only path: a few framework-recognized failures (an oversized request body, a malformed path parameter, a ctx.bind() failure) are converted to a fixed problem+json response before onError is ever consulted, so don't rely on it to customize those specific cases. The OpenAPI layer defaults to validateResponses: false and only turns response validation on when you explicitly opt in outside production, because checking every response against its schema costs real time on every request. The package is still published pre-1.0 (0.2.x), so pin versions and review release notes before moving a production app between versions.",
'response-helpers':
'ctx.ok(), ctx.notFound(), ctx.created(), ctx.problem(), and the rest of the status-code helpers are methods on ServerContext, so a handler returns a Response by calling ctx.<name>(value) rather than constructing new Response(...) by hand. The same functions — json, text, redirect, setCookie, clearCookie, challenge, and friends — are also exported standalone from @askrjs/server for use outside a request context, such as inside an onError handler. setCookie() and clearCookie() take a Response and return a modified one; per the README they only touch response headers and have no opinion on session storage or credential validation.',
},
Expand Down
2 changes: 1 addition & 1 deletion src/pages/docs/usage-guide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1093,7 +1093,7 @@ route('/projects/{projectId}', ProjectPage, {
'Return an explicit allow, redirect, unauthorized, or forbidden decision from the route policy and repeat the enforcement on server APIs.',
`route('/admin', AdminPage, {
policies: [({ auth }) =>
auth.permissions.includes('admin') ? allow() : forbidden()],
auth.principal?.permissions?.includes('admin') ? allow() : forbidden()],
});`,
],
[
Expand Down
8 changes: 4 additions & 4 deletions src/pages/marketing/application-model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ const ownership: readonly SequenceItem[] = [
},
{
title: 'Scopes',
description: 'End effects and resources with their lexical owner.',
meta: 'own · dispose',
description: 'Provide a typed value down the tree without a global store.',
meta: 'provide · read',
},
];

Expand All @@ -43,8 +43,8 @@ export function ApplicationModelPage() {
<h2>Four things, and where each one lives</h2>
<p>
State lives with the component that changes it. Derived values
recompute instead of drifting out of sync. When the owning scope
goes away, its resources are cleaned up automatically.
recompute instead of drifting out of sync. When the owning component
unmounts, its resources are cleaned up automatically.
</p>
</div>
<SequenceList
Expand Down
5 changes: 5 additions & 0 deletions src/pages/marketing/platform.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ export function PlatformPage() {
column is generated from each installed package&rsquo;s published
peer dependencies.
</p>
<p>
All of them are pre-1.0 (0.2.x) — the API surface is still moving,
and <code>askr upgrade</code> exists because breaking changes are
expected between minor versions, not just major ones.
</p>
</div>
<PackageTable label="Published Askr packages" rows={packageRows} />
</RuledSection>
Expand Down
27 changes: 27 additions & 0 deletions tests/docs-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,33 @@ describe('documentation catalog', () => {
expect(prose).not.toContain('manifest-only');
});

it('should keep query and authentication guidance aligned with installed contracts', () => {
const prose = JSON.stringify(headingOverrides);
const symbols = Object.values(apiSymbolSets).flat();
const query = symbols.find(
(symbol) =>
symbol.name === 'Query' &&
symbol.signature.includes('QueryStaleErrorWithValue')
);
const authResolver = symbols.find(
(symbol) => symbol.name === 'AuthResolver'
);
const resolveMember = authResolver?.members?.find(
(member) => member.name === 'resolve'
);

expect(query?.signature).toContain('QueryStaleErrorWithValue');
expect(query?.signature).toContain('QueryStaleError');
expect(prose).toContain('either retains previous data or has data: null');
expect(prose).not.toContain(
'stale query with an error has both data: null'
);

expect(resolveMember?.summary).toContain('fall through as unauthenticated');
expect(prose).toContain('resolves to an unauthenticated context');
expect(prose).not.toContain('not silently treated as anonymous');
});

it('should keep hand-written CLI guidance aligned with generated behavior', () => {
const prose = JSON.stringify(headingOverrides);
expect(prose).not.toContain('nine subcommands');
Expand Down