Give the paging loop, Link-header parser and FieldConfig one home each, rename buildPluginChrome to buildPageChrome, and delete a group from its parents
CI / full-gate (push) Successful in 3m24s

This commit is contained in:
2026-11-09 01:16:55 +01:00
parent a4740c87ae
commit cdb5a00705
17 changed files with 146 additions and 119 deletions
+1 -7
View File
@@ -3,6 +3,7 @@
// *through* us with. Built-in `fetch` only, no SDK dep (AGENTS.md); `fetchImpl`-injectable
// like the kratos/keto clients. We authenticate the user (login) and grant scopes (consent);
// Hydra mints the tokens.
import { nextPageToken } from "./link-header.ts";
export interface OAuth2Client {
client_id?: string;
@@ -101,13 +102,6 @@ export interface HydraAdmin {
rejectLoginRequest(challenge: string, body: RejectRequest): Promise<Completed>;
}
// Hydra paginates with a Link header; pull the page_token of rel="next" (the href is relative, so
// resolve against a throwaway base to read the query param). Mirrors kratos-admin's helper.
function nextPageToken(link: string | null): string | null {
const href = link?.match(/<([^>]+)>\s*;\s*rel="next"/)?.[1];
return href ? new URL(href, "http://hydra").searchParams.get("page_token") : null;
}
export function createHydraAdmin(config: { baseUrl: string; fetchImpl?: typeof fetch }): HydraAdmin {
const base = config.baseUrl.replace(/\/+$/, "");
const http = config.fetchImpl ?? fetch;
+1 -7
View File
@@ -3,6 +3,7 @@
// completion projects Keto permissions into (README). Built-in `fetch` only, no SDK dep (AGENTS.md);
// `fetchImpl`-injectable, reuses kratos-public.ts's `KratosError` (branch on `.status`).
import { KratosError } from "./kratos-public.ts";
import { nextPageToken } from "./link-header.ts";
export interface Identity {
id: string;
@@ -41,13 +42,6 @@ export interface KratosAdmin {
updateMetadataPublic(id: string, metadata: unknown): Promise<Identity>;
}
// Kratos paginates with a Link header; pull the page_token of rel="next" (the href is a
// relative path, so resolve it against a throwaway base just to read the query param).
function nextPageToken(link: string | null): string | null {
const href = link?.match(/<([^>]+)>\s*;\s*rel="next"/)?.[1];
return href ? new URL(href, "http://kratos").searchParams.get("page_token") : null;
}
export function createKratosAdmin(config: { baseUrl: string; fetchImpl?: typeof fetch }): KratosAdmin {
const base = config.baseUrl.replace(/\/+$/, "");
const http = config.fetchImpl ?? fetch;
+14
View File
@@ -0,0 +1,14 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { nextPageToken } from "./link-header.ts";
test("nextPageToken reads the page_token of the rel=next entry among several", () => {
const link = `</admin/identities?page_size=2&page_token=NEXT>; rel="next",</admin/identities?page_size=2>; rel="first"`;
assert.equal(nextPageToken(link), "NEXT");
});
test("nextPageToken is null with no header, no next entry, or a next href without a page_token", () => {
assert.equal(nextPageToken(null), null);
assert.equal(nextPageToken(`</admin/clients?page_size=2>; rel="first"`), null);
assert.equal(nextPageToken(`</admin/clients?page_size=2>; rel="next"`), null);
});
+7
View File
@@ -0,0 +1,7 @@
// Kratos and Hydra both paginate with an RFC 8288 `Link` header. Returns the `page_token` of its
// `rel="next"` entry, or null on the last page. The href is a relative path, so it is resolved
// against a throwaway base just to read the query parameter.
export function nextPageToken(link: string | null): string | null {
const href = link?.match(/<([^>]+)>\s*;\s*rel="next"/)?.[1];
return href ? new URL(href, "http://link-header.invalid").searchParams.get("page_token") : null;
}