@authup/server-auth-console 1.0.0-beta.67 → 1.0.0-beta.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,11 +6,12 @@ import { createHandler } from "@routup/assets";
6
6
  import { basic } from "@routup/basic";
7
7
  import path from "node:path";
8
8
  import { App, defineCoreHandler } from "routup";
9
- import { Client } from "@authup/core-http-kit";
10
- import { getURLBasePath, isUUID } from "@authup/kit";
9
+ import { Client, CookieName, getClientErrorCode } from "@authup/core-http-kit";
10
+ import { getURLBasePath, isObject, isUUID } from "@authup/kit";
11
11
  import { OAuth2UIColorMode } from "@authup/specs";
12
+ import { useRequestCookie } from "@routup/basic/cookie";
12
13
  import { useRequestQuery } from "@routup/basic/query";
13
- import { InternalError } from "@authup/errors";
14
+ import { ErrorCode, InternalError } from "@authup/errors";
14
15
  import { locateUpSync, read } from "locter";
15
16
  import fs from "node:fs";
16
17
  import { fileURLToPath } from "node:url";
@@ -109,6 +110,14 @@ const PAGES = [
109
110
  */
110
111
  const ASSETS_PATH = "/assets";
111
112
  const HEALTH_PATH = "/healthy";
113
+ /**
114
+ * How many forwarded bearers the API may refuse within one window before a
115
+ * render stops forwarding the visitor's access token. Well below server-core's
116
+ * failed-authentication limit (300 a minute per address), so the console's
117
+ * address never reaches it, even across two unaligned windows.
118
+ */
119
+ const SESSION_FORWARDING_REJECTED_MAX = 100;
120
+ const SESSION_FORWARDING_WINDOW = 6e4;
112
121
  //#endregion
113
122
  //#region src/redirect.ts
114
123
  /**
@@ -143,9 +152,10 @@ function sanitizeRelativeRedirect(input) {
143
152
  //#endregion
144
153
  //#region src/payload.ts
145
154
  /**
146
- * The service hydrates ANONYMOUSLY. It holds no credential of its own and
147
- * asks server-core only for what an unauthenticated visitor may see, which
148
- * is the whole of what these pages render.
155
+ * The service holds no credential of its own. Its page data is what an
156
+ * unauthenticated visitor may see; the only authenticated calls are the
157
+ * render's own, made with the visitor's forwarded access token (see
158
+ * `readRenderCookies`) through a client built per render.
149
159
  *
150
160
  * It dispatches against `apiInternalUrl`, never the browser-facing `apiUrl`:
151
161
  * this is a server-side call, so it must not depend on the public address
@@ -306,6 +316,20 @@ function readDeviceFederatedLogin(event) {
306
316
  function readDeviceError(event) {
307
317
  return useRequestQuery(event).error === "access_denied" ? "access_denied" : void 0;
308
318
  }
319
+ /**
320
+ * The request's cookies a render may use to show the visitor's own session:
321
+ * the kit's access token and nothing else.
322
+ *
323
+ * The refresh token is deliberately NOT forwarded. A server render can not
324
+ * hand a rotated pair back to the browser, and refresh tokens rotate strictly,
325
+ * so a refresh here would leave the browser replaying a spent token into
326
+ * family revocation. Without it, a token that no longer verifies simply
327
+ * renders the logged-out page and the browser renews it on hydration.
328
+ */
329
+ function readRenderCookies(event) {
330
+ const accessToken = useRequestCookie(event, CookieName.ACCESS_TOKEN);
331
+ return accessToken ? { [CookieName.ACCESS_TOKEN]: accessToken } : {};
332
+ }
309
333
  //#endregion
310
334
  //#region src/path.ts
311
335
  const CODE_PATH = path.dirname(fileURLToPath(import.meta.url));
@@ -339,6 +363,74 @@ function resolveDistPath(distPath) {
339
363
  if (fs.existsSync(path.join(candidate, "client", "index.html")) && fs.existsSync(path.join(candidate, "server", "server.js"))) return candidate;
340
364
  }
341
365
  //#endregion
366
+ //#region src/session.ts
367
+ /**
368
+ * Whether the API refused a forwarded bearer as a credential nobody issued.
369
+ * It mirrors what server-core's first rate-limit stage counts: an expired or
370
+ * revoked token, or one whose signing key stopped verifying, was issued, so
371
+ * the API does not count it and neither does this guard.
372
+ */
373
+ function isRejectedBearer(err) {
374
+ if (!isObject(err) || !isObject(err.response) || err.response.status !== 401) return false;
375
+ const code = getClientErrorCode(err);
376
+ return code !== ErrorCode.JWT_EXPIRED && code !== ErrorCode.JWT_INACTIVE;
377
+ }
378
+ /**
379
+ * Bounds the bearers a render forwards that the API refuses.
380
+ *
381
+ * A render resolves the visitor's session with their forwarded access token,
382
+ * and on a split deployment every such call reaches the API from this
383
+ * service's one address. server-core counts a refused bearer against the
384
+ * address it came from, so without a bound anyone sending junk access-token
385
+ * cookies would push the console's address over the API's failed
386
+ * authentication limit and lock every hosted page out of the API.
387
+ *
388
+ * A render reserves capacity before it forwards and returns it once it is
389
+ * done, so renders still waiting for the API count against the limit as well:
390
+ * otherwise a burst of parallel renders would all forward before the first
391
+ * refusal came back. Past the limit a page renders without the cookie, which
392
+ * is what a token that does not verify renders anyway; the browser restores
393
+ * the session on hydration.
394
+ */
395
+ function createSessionForwardingGuard(options = {}) {
396
+ const max = options.max ?? 100;
397
+ const window = options.window ?? 6e4;
398
+ const now = options.now ?? (() => Date.now());
399
+ let windowStart = 0;
400
+ let rejected = 0;
401
+ let inflight = 0;
402
+ const roll = () => {
403
+ const time = now();
404
+ if (time - windowStart >= window) {
405
+ windowStart = time;
406
+ rejected = 0;
407
+ }
408
+ };
409
+ return {
410
+ reserve() {
411
+ roll();
412
+ if (rejected + inflight >= max) return;
413
+ inflight += 1;
414
+ let released = false;
415
+ return () => {
416
+ if (released) return;
417
+ released = true;
418
+ inflight -= 1;
419
+ };
420
+ },
421
+ observe(client) {
422
+ client.on("responseError", (err) => {
423
+ if (isRejectedBearer(err)) {
424
+ roll();
425
+ rejected += 1;
426
+ }
427
+ throw err;
428
+ });
429
+ return client;
430
+ }
431
+ };
432
+ }
433
+ //#endregion
342
434
  //#region src/render.ts
343
435
  /**
344
436
  * Refuse a bundle built against another render contract at boot, naming the
@@ -356,6 +448,7 @@ function createRenderPage(distPath) {
356
448
  let cachedHtml;
357
449
  let cachedManifest;
358
450
  let cachedRender;
451
+ const sessionForwarding = createSessionForwardingGuard();
359
452
  return async (event, config, ctx) => {
360
453
  const resolved = cachedDistPath ??= resolveDistPath(distPath);
361
454
  if (!resolved) throw new InternalError("The auth console bundle (@authup/client-auth-console) is not built or installed.");
@@ -375,11 +468,21 @@ function createRenderPage(distPath) {
375
468
  },
376
469
  data: ctx.data
377
470
  };
378
- const [appHtml, preloadLinks] = await render({
379
- url: ctx.url,
380
- manifest,
381
- payload
382
- });
471
+ const cookies = ctx.session ? readRenderCookies(event) : {};
472
+ const release = Object.keys(cookies).length > 0 ? sessionForwarding.reserve() : void 0;
473
+ let appHtml;
474
+ let preloadLinks;
475
+ try {
476
+ [appHtml, preloadLinks] = await render({
477
+ url: ctx.url,
478
+ manifest,
479
+ payload,
480
+ httpClient: sessionForwarding.observe(createAPIClient(config)),
481
+ cookies: release ? cookies : {}
482
+ });
483
+ } finally {
484
+ release?.();
485
+ }
383
486
  let body = replaceTemplateMarker(html, "<!--preload-links-->", preloadLinks);
384
487
  body = replaceTemplateMarker(body, "<!--app-html-->", appHtml);
385
488
  body = stampHtmlAttributes(body, preferences);
@@ -441,7 +544,8 @@ async function createHandler$1(config, themeProvider, render) {
441
544
  fn: async (event) => renderPage(event, config, {
442
545
  url: "/authorize",
443
546
  data: await readAuthorizeInfo(client, event),
444
- theme
547
+ theme,
548
+ session: true
445
549
  })
446
550
  }));
447
551
  for (const page of WORKFLOW_PAGES) app.use(defineCoreHandler({
@@ -513,6 +617,6 @@ function createApplication$1(context = {}) {
513
617
  });
514
618
  }
515
619
  //#endregion
516
- export { CONFIG_SCHEMA as C, VITE_BASE as S, resolveConfig as T, ASSETS_PATH as _, resolveDistPath as a, HEALTH_PATH as b, createAPIClient as c, readDeviceError as d, readDeviceFederatedLogin as f, sanitizeRelativeRedirect as g, readUILocalesHint as h, createRenderPage as i, createFeaturesReader as l, readUIColorModeHint as m, createHandler$1 as n, resolvePackagePath as o, readDeviceUserCode as p, assertRenderContract as r, buildWorkflowPageData as s, createApplication$1 as t, readAuthorizeInfo as u, BASE_PATH as v, readConfigFromEnv as w, PAGES as x, CONTRACT_VERSION as y };
620
+ export { PAGES as C, CONFIG_SCHEMA as D, VITE_BASE as E, readConfigFromEnv as O, HEALTH_PATH as S, SESSION_FORWARDING_WINDOW as T, readUILocalesHint as _, createSessionForwardingGuard as a, BASE_PATH as b, buildWorkflowPageData as c, readAuthorizeInfo as d, readDeviceError as f, readUIColorModeHint as g, readRenderCookies as h, createRenderPage as i, resolveConfig as k, createAPIClient as l, readDeviceUserCode as m, createHandler$1 as n, resolveDistPath as o, readDeviceFederatedLogin as p, assertRenderContract as r, resolvePackagePath as s, createApplication$1 as t, createFeaturesReader as u, sanitizeRelativeRedirect as v, SESSION_FORWARDING_REJECTED_MAX as w, CONTRACT_VERSION as x, ASSETS_PATH as y };
517
621
 
518
- //# sourceMappingURL=application-h8KbQSdz.mjs.map
622
+ //# sourceMappingURL=application-DvnUIu4h.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"application-DvnUIu4h.mjs","names":["createHandler","createAssetsHandler","createApplication","createConsoleApplication","createHandler"],"sources":["../src/config.ts","../src/constants.ts","../src/redirect.ts","../src/payload.ts","../src/path.ts","../src/resolve.ts","../src/session.ts","../src/render.ts","../src/handler.ts","../src/application.ts"],"sourcesContent":["/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport {\n AUTH_CONSOLE_SCHEMA,\n CORE_SCHEMA,\n ROOT_SCHEMA,\n SECTION_KEY,\n THEME_SCHEMA,\n} from '@authup/server-config';\nimport {\n buildSchemaDefaults,\n defineSchema,\n mergeSchemaData,\n mountSchema,\n readSchemaFromEnv,\n resolveSchemaData,\n} from '@authup/server-config-kit';\nimport { Container } from 'validup';\nimport type { CoreConfig, EnvironmentVariable } from '@authup/server-config';\nimport type { Config, ConfigInput } from './types';\n\n/**\n * The keys this service reads, SELECTED out of the document schema by name.\n *\n * Nothing is declared here: every key of `authup.yml` is declared once in\n * `@authup/server-config`, so this service cannot spell a path, an\n * environment variable, a default or a reader differently from server-core,\n * which reads several of the same keys. Neither package depends on the other;\n * both depend on the declaration.\n *\n * Its own section is spread, so this service reads it in its own vocabulary\n * (`url`, `port`, `host`); every other section stays under the key the\n * document nests it at.\n */\nexport const CONFIG_SCHEMA = defineSchema<ConfigInput, 'publicUrl' | 'internalUrl' | 'db'>({\n ...AUTH_CONSOLE_SCHEMA,\n ...ROOT_SCHEMA,\n [SECTION_KEY.THEME]: THEME_SCHEMA,\n // Two keys of core, never the section. `publicUrl` derives from\n // the core listener address, which is what a console needs; selecting the\n // whole section also selects its INVARIANTS, and `resolveSchemaData` runs\n // every resolver in a registry -- so a console would refuse to start on a\n // short SECRETS_ENCRYPTION_KEY, or on mfaRequired without mfaEnabled, for\n // keys it has no database, key store or MFA to use.\n [SECTION_KEY.CORE]: defineSchema<Pick<CoreConfig, 'host' | 'port'>, never, EnvironmentVariable>({\n host: CORE_SCHEMA.host,\n port: CORE_SCHEMA.port,\n }),\n});\n\n/**\n * Turn the configuration namespace into the service's own shape: validate the\n * input against the declared zod types, fill the defaults, derive the one key\n * that is derived rather than configured, and rename. An empty `url` means the console sits on server-core's own origin\n * under the default segment, which is the single-origin deployment.\n *\n * The defaults are layered SECTION-AWARE ({@link mergeSchemaData}): a spread\n * would let an input carrying one key of a section replace the whole section\n * and take every other key's default with it.\n */\nexport async function resolveConfig(\n input: Partial<ConfigInput>,\n) : Promise<Config> {\n const validator = new Container<ConfigInput>();\n mountSchema(validator, CONFIG_SCHEMA);\n\n const parsed = await validator.run(input);\n\n const values = resolveSchemaData<ConfigInput>(\n CONFIG_SCHEMA,\n mergeSchemaData<ConfigInput>(\n CONFIG_SCHEMA,\n buildSchemaDefaults<ConfigInput>(CONFIG_SCHEMA),\n parsed,\n ),\n ) as ConfigInput;\n\n return {\n url: values.url,\n apiUrl: values.publicUrl,\n // `internalUrl` falls back to `publicUrl`, so an operator who has one\n // address for both sides configures nothing. The composed `authup\n // start` overrides this with its own listen address unless the\n // document named one.\n apiInternalUrl: values.internalUrl,\n port: values.port,\n host: values.host,\n distPath: values.path,\n theme: values.theme,\n };\n}\n\n/**\n * The standalone entry's configuration, read from the environment alone.\n * `authup.yml` reaches this service through the CLI roles, which compose this\n * very registry into the one document loader.\n */\nexport async function readConfigFromEnv() : Promise<Config> {\n // The explicit type argument is load-bearing: inferred from the schema\n // object, a key declared without a default (the derived publicUrl) comes\n // back as unknown.\n const input : Partial<ConfigInput> = readSchemaFromEnv<ConfigInput>(CONFIG_SCHEMA);\n\n return resolveConfig(input);\n}\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport type { CONTRACT_VERSION as BundleContractVersion } from '@authup/client-auth-console';\n\n/**\n * The render contract this service compiles against. The literal type fails\n * the build the moment the bundle's constant moves without this one.\n */\nexport const CONTRACT_VERSION : typeof BundleContractVersion = 5;\n\n/**\n * The vite base the auth console bundle is built with. Asset hrefs in the\n * shell are emitted against it, so the service mounts its assets there and\n * rebases them only when the deployment carries a sub-path.\n */\nexport const VITE_BASE = '/console/auth/';\n\n/**\n * The path the service is served under by default, i.e. the path component\n * of `authConsole.url`. It is the same segment server-core mounted\n * the assets at before the split, so a built bundle needs no rebuild.\n */\nexport const BASE_PATH = '/console/auth';\n\n/**\n * The pages the service renders. Each is a route of the console bundle's\n * own router, and each is what a server-core page GET now redirects to.\n */\nexport const PAGES = [\n '/authorize',\n '/register',\n '/activate',\n '/password-forgot',\n '/password-reset',\n '/logout',\n '/device',\n] as const;\n\n/**\n * Where this service mounts the bundle's `assets/` directory. The public\n * asset URL is the service's own public path plus this route, which is what\n * the bundle's fixed vite base is rebased onto.\n */\nexport const ASSETS_PATH = '/assets';\n\nexport const HEALTH_PATH = '/healthy';\n\n/**\n * How many forwarded bearers the API may refuse within one window before a\n * render stops forwarding the visitor's access token. Well below server-core's\n * failed-authentication limit (300 a minute per address), so the console's\n * address never reaches it, even across two unaligned windows.\n */\nexport const SESSION_FORWARDING_REJECTED_MAX = 100;\n\nexport const SESSION_FORWARDING_WINDOW = 60 * 1000;\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\n/**\n * Open-redirect guard for the `redirect` query parameter carried by the\n * auth workflow pages: only same-origin relative paths pass through, so the\n * value can be safely reflected into an anchor href / forwarded as a\n * post-login destination.\n *\n * Rejects: non-strings, control/whitespace chars (browsers strip some,\n * shifting where the URL authority begins), protocol-relative (`//host`),\n * backslash variants (browsers normalise `\\` → `/`), and any scheme\n * (`javascript:`, `http:`) — both on the raw and once-decoded form so a\n * percent-encoded separator (`%2f`, `%5c`) can't slip past.\n */\nexport function sanitizeRelativeRedirect(input: unknown): string | undefined {\n if (typeof input !== 'string' || input.length === 0) {\n return undefined;\n }\n\n // Raw input: reject control chars AND whitespace (space/tab/newline) — a\n // raw space can shift where the URL authority begins.\n // eslint-disable-next-line no-control-regex\n if (/[\\u0000-\\u0020\\u007f]/.test(input)) {\n return undefined;\n }\n\n let decoded: string;\n try {\n decoded = decodeURIComponent(input);\n } catch {\n return undefined;\n }\n\n // Decoded form: reject C0 controls + DEL (CR/LF/NUL — the header-injection\n // vectors a `%0a`/`%0d` would smuggle past the raw check) but NOT space\n // (0x20), so a legitimate `%20` in the path still passes.\n // eslint-disable-next-line no-control-regex\n if (/[\\u0000-\\u001f\\u007f]/.test(decoded)) {\n return undefined;\n }\n\n for (const candidate of [input, decoded]) {\n if (!candidate.startsWith('/')) {\n return undefined;\n }\n if (candidate.length > 1 && (candidate[1] === '/' || candidate[1] === '\\\\')) {\n return undefined;\n }\n if (candidate.includes('\\\\')) {\n return undefined;\n }\n }\n\n return input;\n}\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport type { AuthorizeInfo, StatusResponse, StatusResponseFeatures } from '@authup/core-http-kit';\nimport { Client, CookieName } from '@authup/core-http-kit';\nimport { isUUID } from '@authup/kit';\nimport { OAuth2UIColorMode } from '@authup/specs';\nimport { useRequestCookie } from '@routup/basic/cookie';\nimport { useRequestQuery } from '@routup/basic/query';\nimport type { IAppEvent } from 'routup';\nimport { sanitizeRelativeRedirect } from './redirect';\nimport type { Config } from './types';\n\n/**\n * The service holds no credential of its own. Its page data is what an\n * unauthenticated visitor may see; the only authenticated calls are the\n * render's own, made with the visitor's forwarded access token (see\n * `readRenderCookies`) through a client built per render.\n *\n * It dispatches against `apiInternalUrl`, never the browser-facing `apiUrl`:\n * this is a server-side call, so it must not depend on the public address\n * resolving from inside the deployment. Unlike server-core's own internal\n * client the swap is on `baseURL` rather than at the transport layer,\n * because nothing derived from it reaches a caller -- there is no\n * `redirect_uri` here to be compared byte for byte later.\n */\nexport function createAPIClient(config: Config) : Client {\n return new Client({ baseURL: config.apiInternalUrl });\n}\n\n/**\n * Report an outbound failure to the operator and raise one the visitor may\n * see, which is deliberately not the same error.\n *\n * hapic embeds the resolved request URL in its message, and this service has\n * no error middleware: routup answers with `event.error.message` verbatim, on\n * the public login page. That URL is `apiInternalUrl`, which on a split\n * deployment names a service on the operator's own network, so an API that is\n * merely down would publish its internal address to every anonymous visitor.\n * It was harmless while the value was `publicUrl`, an address the browser\n * already had.\n *\n * The rule and the remedy are server-core's own (`sanitizeError` /\n * `describeError`): the caller gets a message it can act on, the detail goes\n * to the log. `console.error` rather than a logger because a console handler\n * is built from a config alone and is given none -- and because this line is\n * the only trace such a failure leaves, which is how #3550 was diagnosed in\n * the first place.\n */\nfunction failWithoutAddress(error: unknown) : never {\n // eslint-disable-next-line no-console\n console.error('[authup] the auth console could not reach the API:', error);\n\n throw new Error('The identity provider could not be reached.');\n}\n\n/**\n * The feature flags every workflow page gates its form on. A page that\n * cannot reach them must not silently claim a workflow is disabled, so the\n * failure propagates rather than defaulting to false.\n *\n * Read ONCE and held for the reader's lifetime, the same shape render.ts\n * keeps for the dist template, manifest and render entry: `GET /` reports\n * the version and the boot-config feature flags, none of which changes\n * without a restart, so a TTL would be a number with nothing behind it.\n * Without it every workflow page render is an API call, and under the\n * composed `authup start` those all arrive over loopback on one shared\n * rate-limit key. Only a SUCCESS is held, so an API that was down at the\n * first render is retried rather than remembered as broken.\n *\n * The cache is instance-scoped rather than module-scoped (098 C4): the\n * answer depends on which API the client was built against, so two handlers\n * in one process must not share one, and a module slot would additionally\n * leak between test cases.\n */\nexport function createFeaturesReader(client: Client) : () => Promise<StatusResponseFeatures> {\n let cached : StatusResponseFeatures | undefined;\n let pending : Promise<StatusResponseFeatures> | undefined;\n\n return () => {\n if (cached) {\n return Promise.resolve(cached);\n }\n\n if (pending) {\n return pending;\n }\n\n pending = client.status.get()\n .then((status: StatusResponse) => {\n cached = status.features;\n\n return cached;\n })\n .catch((e) => failWithoutAddress(e));\n\n const clear = () => {\n pending = undefined;\n };\n pending.then(clear, clear);\n\n return pending;\n };\n}\n\n/**\n * The `/authorize` render input, resolved by server-core (plan 101 D2-1).\n * The browser's query is forwarded verbatim: the answer is derived from all\n * of it, the `provider` hint and the closed `error` marker set included.\n */\nexport async function readAuthorizeInfo(client: Client, event: IAppEvent) : Promise<AuthorizeInfo> {\n const { search } = new URL(event.request.url);\n\n try {\n return await client.authorize.getInfo(search);\n } catch (e) {\n return failWithoutAddress(e);\n }\n}\n\nexport type WorkflowPageOptions = {\n // Whether the page consumes the `realmId` (legacy `realm_id`) / `token`\n // query params (e.g. prefill from an email deep link). Off by default so\n // a page never reflects a param it doesn't use.\n realmAware?: boolean,\n tokenAware?: boolean,\n};\n\n/**\n * The payload the four workflow pages render from: the feature flags plus\n * the handful of query params each one declares. Unchanged from the\n * assembly server-core did before the split, including the open-redirect\n * guard on `redirect`.\n */\nexport function buildWorkflowPageData(\n event: IAppEvent,\n features: StatusResponseFeatures,\n options: WorkflowPageOptions = {},\n) : Record<string, any> {\n const query = useRequestQuery(event);\n\n const data: Record<string, any> = {\n features,\n redirect: sanitizeRelativeRedirect(query.redirect),\n };\n\n if (options.realmAware) {\n const realmId = typeof query.realmId === 'string' ? query.realmId : query.realm_id;\n data.realmId = typeof realmId === 'string' ? realmId : undefined;\n }\n\n if (options.tokenAware) {\n data.token = typeof query.token === 'string' ? query.token : undefined;\n }\n\n return data;\n}\n\n/**\n * The RP's own UI language, from the OIDC Core section 3.1.2.1 `ui_locales`\n * parameter: a space-separated list of BCP47 tags in preference order.\n *\n * It rides the authorization request, so it survives the page GET's hop and\n * the rebuild a federated round-trip makes from the stored code request. The\n * four workflow pages take it straight from their own query, since an RP\n * linking to one can say the same thing there.\n */\nexport function readUILocalesHint(event: IAppEvent) : string | undefined {\n const { ui_locales: uiLocales } = useRequestQuery(event);\n if (typeof uiLocales !== 'string') {\n return undefined;\n }\n\n // ponytail: the first well-formed tag wins, and it is NOT narrowed to a\n // locale authup has a catalog for. That is the latitude the navigator\n // path already takes -- a `de-CH` visitor is stamped `lang=\"de-CH\"` and\n // reads the `de` catalog. Narrow here (matchLocale over LOCALES) if the\n // lang attribute ever has to name only an authored locale, which costs\n // this service a dependency on @authup/i18n.\n return uiLocales\n .split(' ')\n .find((tag) => /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]+)*$/.test(tag));\n}\n\n/**\n * The color mode the RP renders in, from authup's own `ui_color_mode`. One\n * value rather than a list, since there is nothing to negotiate.\n *\n * Checked against the enum rather than local literals because the realm's\n * discovery document ADVERTISES that same set as\n * `ui_color_modes_supported`: a second spelling here would let the pages and\n * the document disagree about what the deployment accepts.\n */\nexport function readUIColorModeHint(event: IAppEvent) : string | undefined {\n const { ui_color_mode: uiColorMode } = useRequestQuery(event);\n\n return typeof uiColorMode === 'string' &&\n (Object.values(OAuth2UIColorMode) as string[]).includes(uiColorMode) ?\n uiColorMode :\n undefined;\n}\n\n/**\n * The `user_code` the device page prefills (RFC 8628 section 3.3.1): the\n * query value uppercased and stripped of everything outside `A-Z0-9`, or\n * `undefined` when nothing bounded remains. The page renders it back, so what\n * reaches the payload is at most 16 alphanumeric characters.\n */\nexport function readDeviceUserCode(event: IAppEvent) : string | undefined {\n const query = useRequestQuery(event);\n if (typeof query.user_code !== 'string') {\n return undefined;\n }\n\n const canonical = query.user_code.toUpperCase().replace(/[^A-Z0-9]/g, '');\n if (canonical.length === 0 || canonical.length > 16) {\n return undefined;\n }\n\n return canonical;\n}\n\n/**\n * The provider a federated login on the device page came back from (#3589).\n * A routing hint and no secret: the pending login itself rides an HttpOnly\n * cookie. Anything that is not a uuid is dropped, the rule `/authorize`\n * reads its own hint by: the page interpolates the value into the\n * completion request path.\n */\nexport function readDeviceFederatedLogin(event: IAppEvent) : { providerId: string } | undefined {\n const { provider } = useRequestQuery(event);\n\n return typeof provider === 'string' && isUUID(provider) ?\n { providerId: provider } :\n undefined;\n}\n\n/**\n * The refusal marker the federated callback may attach. A closed set of one,\n * so nothing request-derived is rendered.\n */\nexport function readDeviceError(event: IAppEvent) : 'access_denied' | undefined {\n return useRequestQuery(event).error === 'access_denied' ? 'access_denied' : undefined;\n}\n\n/**\n * The request's cookies a render may use to show the visitor's own session:\n * the kit's access token and nothing else.\n *\n * The refresh token is deliberately NOT forwarded. A server render can not\n * hand a rotated pair back to the browser, and refresh tokens rotate strictly,\n * so a refresh here would leave the browser replaying a spent token into\n * family revocation. Without it, a token that no longer verifies simply\n * renders the logged-out page and the browser renews it on hydration.\n */\nexport function readRenderCookies(event: IAppEvent) : Record<string, string> {\n const accessToken = useRequestCookie(event, CookieName.ACCESS_TOKEN);\n\n return accessToken ? { [CookieName.ACCESS_TOKEN]: accessToken } : {};\n}\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nexport const CODE_PATH = path.dirname(fileURLToPath(import.meta.url));\nexport const PACKAGE_PATH = path.join(CODE_PATH, '..');\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport { locateUpSync } from 'locter';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { PACKAGE_PATH } from './path';\n\n/**\n * Locate the auth console package: a substituted one (config `distPath`)\n * first, else the SSR auth workflow UI shipped as\n * `@authup/client-auth-console` (a dependency of THIS service), which the\n * node_modules ancestor walk from this package's own root (locter's\n * `locateUp`) finds for the workspace (symlink onto\n * apps/client-auth-console) and for a published install alike.\n *\n * The `cwd` anchor is behavioural, not incidental: it decides which\n * node_modules tree is walked, so it stays pinned to this package rather\n * than to the process cwd.\n */\nexport function resolvePackagePath(distPath?: string) : string | undefined {\n return distPath || locateUpSync(\n 'node_modules/@authup/client-auth-console/package.json',\n { cwd: PACKAGE_PATH },\n )?.directory;\n}\n\n/**\n * Locate the BUILT auth console bundle (`dist/client` template + assets,\n * `dist/server/server.js` render entry). A half-built package is no bundle:\n * without the render entry there is nothing to render with.\n */\nexport function resolveDistPath(distPath?: string) : string | undefined {\n const packagePath = resolvePackagePath(distPath);\n if (!packagePath) {\n return undefined;\n }\n\n const candidate = path.join(packagePath, 'dist');\n if (\n fs.existsSync(path.join(candidate, 'client', 'index.html')) &&\n fs.existsSync(path.join(candidate, 'server', 'server.js'))\n ) {\n return candidate;\n }\n\n return undefined;\n}\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport { getClientErrorCode } from '@authup/core-http-kit';\nimport type { Client } from '@authup/core-http-kit';\nimport { ErrorCode } from '@authup/errors';\nimport { isObject } from '@authup/kit';\nimport { SESSION_FORWARDING_REJECTED_MAX, SESSION_FORWARDING_WINDOW } from './constants';\nimport type { SessionForwardingGuard, SessionForwardingGuardOptions } from './types';\n\n/**\n * Whether the API refused a forwarded bearer as a credential nobody issued.\n * It mirrors what server-core's first rate-limit stage counts: an expired or\n * revoked token, or one whose signing key stopped verifying, was issued, so\n * the API does not count it and neither does this guard.\n */\nfunction isRejectedBearer(err: unknown) : boolean {\n if (!isObject(err) || !isObject(err.response) || err.response.status !== 401) {\n return false;\n }\n\n const code = getClientErrorCode(err);\n return code !== ErrorCode.JWT_EXPIRED && code !== ErrorCode.JWT_INACTIVE;\n}\n\n/**\n * Bounds the bearers a render forwards that the API refuses.\n *\n * A render resolves the visitor's session with their forwarded access token,\n * and on a split deployment every such call reaches the API from this\n * service's one address. server-core counts a refused bearer against the\n * address it came from, so without a bound anyone sending junk access-token\n * cookies would push the console's address over the API's failed\n * authentication limit and lock every hosted page out of the API.\n *\n * A render reserves capacity before it forwards and returns it once it is\n * done, so renders still waiting for the API count against the limit as well:\n * otherwise a burst of parallel renders would all forward before the first\n * refusal came back. Past the limit a page renders without the cookie, which\n * is what a token that does not verify renders anyway; the browser restores\n * the session on hydration.\n */\nexport function createSessionForwardingGuard(\n options: SessionForwardingGuardOptions = {},\n) : SessionForwardingGuard {\n const max = options.max ?? SESSION_FORWARDING_REJECTED_MAX;\n const window = options.window ?? SESSION_FORWARDING_WINDOW;\n const now = options.now ?? (() => Date.now());\n\n let windowStart = 0;\n let rejected = 0;\n let inflight = 0;\n\n const roll = () => {\n const time = now();\n if (time - windowStart >= window) {\n windowStart = time;\n rejected = 0;\n }\n };\n\n return {\n reserve() {\n roll();\n if (rejected + inflight >= max) {\n return undefined;\n }\n\n inflight += 1;\n\n let released = false;\n return () => {\n if (released) {\n return;\n }\n\n released = true;\n inflight -= 1;\n };\n },\n observe(client: Client) {\n client.on('responseError', (err) => {\n if (isRejectedBearer(err)) {\n roll();\n rejected += 1;\n }\n\n throw err;\n });\n\n return client;\n },\n };\n}\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport type { HydrationPayload, RenderFunction } from '@authup/client-auth-console';\nimport { InternalError } from '@authup/errors';\nimport { getURLBasePath } from '@authup/kit';\nimport {\n applyTheme,\n applyUIPageHeaders,\n readUIClientPreferences,\n rebaseAssetURLs,\n replaceTemplateMarker,\n stampHtmlAttributes,\n} from '@authup/server-console-kit';\nimport { read } from 'locter';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { CONTRACT_VERSION, VITE_BASE } from './constants';\nimport {\n createAPIClient,\n readRenderCookies,\n readUIColorModeHint,\n readUILocalesHint,\n} from './payload';\nimport { resolveDistPath } from './resolve';\nimport { createSessionForwardingGuard } from './session';\nimport type { RenderPage } from './types';\n\n/**\n * Refuse a bundle built against another render contract at boot, naming the\n * entry and both versions, instead of failing per request on `/authorize`.\n * A bundle predating the export is version 1. Reads the module itself, which\n * Node's ESM cache then hands the render for free.\n */\nexport async function assertRenderContract(distPath: string) : Promise<void> {\n const entry = path.join(distPath, 'server', 'server.js');\n const bundle = await read(entry) as Record<string, any>;\n const version = bundle.CONTRACT_VERSION ?? 1;\n\n if (version !== CONTRACT_VERSION) {\n throw new InternalError(\n `The auth console bundle \"${entry}\" implements render-contract version ${version}, ` +\n `but this service requires ${CONTRACT_VERSION}. ` +\n 'Rebuild it against the current @authup/client-auth-console contract.',\n );\n }\n}\n\nexport function createRenderPage(distPath?: string) : RenderPage {\n // Per-handler caches for the immutable production SSR assets: the dist\n // template, manifest and server bundle do not change after boot, so read\n // them once instead of per request (the auth/login routes are hot paths).\n // Held in this closure rather than at module scope so a second handler in\n // the process, with a substituted package, never renders another's\n // template. Only a resolved dist is kept, so a bundle built after boot is\n // picked up on the next request.\n let cachedDistPath: string | undefined;\n let cachedHtml: string | undefined;\n let cachedManifest: Record<string, any> | undefined;\n let cachedRender: RenderFunction | undefined;\n // Per handler as well: the refused-bearer count belongs to the address\n // this handler's renders reach the API from.\n const sessionForwarding = createSessionForwardingGuard();\n\n return async (event, config, ctx) => {\n const resolved = (cachedDistPath ??= resolveDistPath(distPath));\n if (!resolved) {\n throw new InternalError(\n 'The auth console bundle (@authup/client-auth-console) is not built or installed.',\n );\n }\n\n const html = (cachedHtml ??= await fs.promises.readFile(\n path.join(resolved, 'client', 'index.html'),\n 'utf-8',\n ));\n const manifest = (cachedManifest ??= JSON.parse(await fs.promises.readFile(\n path.join(resolved, 'client', '.vite', 'ssr-manifest.json'),\n 'utf-8',\n )));\n const render = (cachedRender ??= (await read(\n path.join(resolved, 'server', 'server.js'),\n )).render as RenderFunction);\n\n // The client app hydrates locale + color-mode from the payload, so it\n // adopts the same values the HTML shell is stamped with below.\n const preferences = readUIClientPreferences(event);\n\n // `ui_locales` is what the RP says the visitor is reading over there,\n // which is fresher than anything this origin knows about a visitor\n // arriving for the first time. It seeds the page only while they have\n // made no choice HERE: `vc-locale` holds `auto` until the switcher\n // writes a tag (@vuecs/locale never writes the resolved value back),\n // so an explicit choice keeps winning and the switcher cannot be\n // undone by the next authorize request carrying the hint. Seeding is\n // all it does -- the ref starts there, and nothing writes it back.\n if (!preferences.locale || preferences.locale === 'auto') {\n preferences.locale = readUILocalesHint(event) || preferences.locale;\n }\n\n // `ui_color_mode` the same way, and `system` is its `auto`: the\n // switcher is a binary toggle, so it only ever writes `light` or\n // `dark`, and anything else in the cookie means the visitor never\n // touched it here.\n if (!preferences.colorMode || preferences.colorMode === 'system') {\n preferences.colorMode = readUIColorModeHint(event) || preferences.colorMode;\n }\n\n // `baseURL` is server-core's public URL: the console derives its HTTP\n // client and its cookie path from it. `basePath` is where THIS service\n // is served, which is what the router and every inter-page href need.\n // Before the split the two were the same value; they are not any more,\n // so the console reads them separately.\n const basePath = getURLBasePath(config.url);\n\n const payload : HydrationPayload = {\n config: {\n baseURL: config.apiUrl,\n basePath,\n colorMode: preferences.colorMode,\n locale: preferences.locale,\n },\n data: ctx.data,\n };\n\n // A render that forwards the visitor's token holds a slot of the guard\n // until it is done, refused or not. A visitor without one holds none,\n // or ordinary anonymous traffic would use up the slots.\n const cookies = ctx.session ? readRenderCookies(event) : {};\n const release = Object.keys(cookies).length > 0 ? sessionForwarding.reserve() : undefined;\n\n let appHtml : string;\n let preloadLinks : string;\n try {\n [appHtml, preloadLinks] = await render({\n url: ctx.url,\n manifest,\n payload,\n // One client per render: it carries the visitor's bearer once the\n // store has resolved their session, so it must never outlive the\n // request. Built against the internal address like every other\n // server-side call (#3550).\n httpClient: sessionForwarding.observe(createAPIClient(config)),\n cookies: release ? cookies : {},\n });\n } finally {\n release?.();\n }\n\n let body = replaceTemplateMarker(html, '<!--preload-links-->', preloadLinks);\n body = replaceTemplateMarker(body, '<!--app-html-->', appHtml);\n\n body = stampHtmlAttributes(body, preferences);\n\n // This service mounts the bundle's assets at its own /assets, so the\n // fixed vite base in every href is replaced by this service's public\n // path. The vite base was decided when the bundle was built and says\n // nothing about where the service is published.\n body = rebaseAssetURLs(body, VITE_BASE, `${basePath}/`);\n\n // The theme's own asset urls are built from the same base, and the\n // service mounts them under it, so a themed deployment needs no rule of\n // its own at the reverse proxy beyond the one that reaches this service.\n body = await applyTheme(body, ctx.theme, basePath);\n\n applyUIPageHeaders(event);\n\n return body;\n };\n}\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport type { IThemeProvider } from '@authup/server-console-kit';\nimport {\n THEME_ASSET_MOUNT_PATH,\n createThemeAssetsHandler,\n createThemeProvider,\n} from '@authup/server-console-kit';\nimport { createHandler as createAssetsHandler } from '@routup/assets';\nimport { basic } from '@routup/basic';\nimport path from 'node:path';\nimport type { IApp } from 'routup';\nimport { App, defineCoreHandler } from 'routup';\nimport { ASSETS_PATH, HEALTH_PATH } from './constants';\nimport {\n buildWorkflowPageData,\n createAPIClient,\n createFeaturesReader,\n readAuthorizeInfo,\n readDeviceError,\n readDeviceFederatedLogin,\n readDeviceUserCode,\n} from './payload';\nimport { assertRenderContract, createRenderPage } from './render';\nimport { resolveDistPath } from './resolve';\nimport type { Config, RenderPage } from './types';\n\nconst WORKFLOW_PAGES : {\n url: string,\n realmAware?: boolean,\n tokenAware?: boolean\n}[] = [\n { url: '/register', realmAware: true },\n { url: '/activate', tokenAware: true },\n { url: '/password-forgot', realmAware: true },\n {\n url: '/password-reset',\n realmAware: true,\n tokenAware: true,\n },\n];\n\n/**\n * The service as a mountable routup handler, so the CLI can compose it onto\n * server-core's own listener for the single-container deployment while a\n * split deployment runs it behind its own server.\n *\n * The paths are prefix-free. authup assumes a prefix-stripping reverse\n * proxy, so a service published at `<origin>/console/auth` receives\n * `/authorize`, exactly as server-core received it before the split.\n */\nexport async function createHandler(\n config: Config,\n themeProvider?: IThemeProvider,\n render?: RenderPage,\n) : Promise<IApp> {\n const renderPage = render ?? createRenderPage(config.distPath);\n\n const app = new App();\n const client = createAPIClient(config);\n const readFeatures = createFeaturesReader(client);\n\n // The shell is stamped from the vc-locale / vc-color-mode cookies, and\n // without the plugin every cookie read answers undefined, which is a\n // silent flash of the wrong color mode on every full load. server-core\n // registers it globally, so a mounted handler inherits one; a standalone\n // service has nothing above it.\n app.use(basic({ cookie: true, query: true }));\n\n // A missing directory disables theming entirely: no provider is created\n // and the rendered pages are byte-identical to the un-themed ones, so the\n // default configuration pays nothing. An invalid manifest throws here and\n // fails the boot rather than surfacing per request.\n // Injected by the theme module when this runs inside the console\n // application; built here for a caller holding only a config.\n const theme = themeProvider ?? await createThemeProvider(config);\n\n app.use(defineCoreHandler({\n method: 'get',\n path: HEALTH_PATH,\n fn: () => ({ status: 'ok' }),\n }));\n\n if (theme) {\n app.use(THEME_ASSET_MOUNT_PATH, createThemeAssetsHandler(theme));\n }\n\n app.use(defineCoreHandler({\n method: 'get',\n path: '/authorize',\n fn: async (event) => renderPage(event, config, {\n url: '/authorize',\n data: await readAuthorizeInfo(client, event),\n theme,\n // The one page whose first paint depends on the visitor's\n // session (the account chooser, consent, the challenge). Every\n // other page opens on the same step signed in or not, so\n // resolving a session there would only cost API calls.\n session: true,\n }),\n }));\n\n for (const page of WORKFLOW_PAGES) {\n app.use(defineCoreHandler({\n method: 'get',\n path: page.url,\n fn: async (event) => {\n const features = await readFeatures();\n\n return renderPage(event, config, {\n url: page.url,\n data: buildWorkflowPageData(event, features, page),\n theme,\n });\n },\n }));\n }\n\n // The logout page needs nothing resolved up front: it drives the\n // end-session call itself and renders from the answer, so the render\n // stays a pure shell and no request reaches server-core here.\n app.use(defineCoreHandler({\n method: 'get',\n path: '/logout',\n fn: async (event) => renderPage(event, config, {\n url: '/logout',\n data: {},\n theme,\n }),\n }));\n\n app.use(defineCoreHandler({\n method: 'get',\n path: '/device',\n fn: async (event) => renderPage(event, config, {\n url: '/device',\n data: {\n features: await readFeatures(),\n userCode: readDeviceUserCode(event),\n federatedLogin: readDeviceFederatedLogin(event),\n error: readDeviceError(event),\n },\n theme,\n }),\n }));\n\n // Only the assets directory is mounted, never dist/client itself: the\n // template and the ssr manifest are inputs of the render, not files to\n // serve. A missing bundle only disables the mount; the page routes\n // report the actionable error. A resolved one is checked against the\n // render contract here, unless a substituted render never reads it.\n const distPath = resolveDistPath(config.distPath);\n if (distPath) {\n if (!render) {\n await assertRenderContract(distPath);\n }\n\n // Every name carries a content hash, so a new build means new names.\n app.use(ASSETS_PATH, createAssetsHandler(\n path.posix.join(distPath, 'client', 'assets'),\n {\n fallthrough: false,\n scan: false,\n cacheMaxAge: 60 * 60 * 24 * 365,\n cacheImmutable: true,\n },\n ));\n }\n\n return app;\n}\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport { createApplication as createConsoleApplication } from '@authup/server-console-kit';\nimport type { CreateApplicationContext } from '@authup/server-console-kit';\nimport type { Application } from 'orkos';\nimport { readConfigFromEnv } from './config';\nimport { createHandler } from './handler';\nimport type { Config } from './types';\n\n/**\n * This console as a runnable service: its own module graph, its own\n * container, its own lifecycle, and nothing of server-core's. It is what\n * `authup start console` starts, what the `authup-auth-console` bin starts, and what\n * `authup start` composes onto its own listener, so no caller reaches past\n * the graph for a bare handler.\n *\n * `config` defaults to a FACTORY, not a value: resolving it reads the\n * document, and a console started alongside others must not do that at\n * construction time, before the caller has said where to look. Pass\n * `listen: false` to be mounted on someone else's listener.\n */\nexport function createApplication(\n context: Partial<Omit<CreateApplicationContext<Config>, 'createHandler'>> = {},\n) : Application {\n return createConsoleApplication<Config>({\n listen: context.listen,\n config: context.config ?? readConfigFromEnv,\n createHandler: (resolved, theme) => createHandler(resolved, theme),\n });\n}\n"],"mappings":"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"}
package/dist/bin.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as createApplication } from "./application-h8KbQSdz.mjs";
2
+ import { t as createApplication } from "./application-DvnUIu4h.mjs";
3
3
  import process from "node:process";
4
4
  //#region src/bin.ts
5
5
  /**
@@ -28,4 +28,12 @@ export declare const PAGES: readonly ["/authorize", "/register", "/activate", "/
28
28
  */
29
29
  export declare const ASSETS_PATH = "/assets";
30
30
  export declare const HEALTH_PATH = "/healthy";
31
+ /**
32
+ * How many forwarded bearers the API may refuse within one window before a
33
+ * render stops forwarding the visitor's access token. Well below server-core's
34
+ * failed-authentication limit (300 a minute per address), so the console's
35
+ * address never reaches it, even across two unaligned windows.
36
+ */
37
+ export declare const SESSION_FORWARDING_REJECTED_MAX = 100;
38
+ export declare const SESSION_FORWARDING_WINDOW: number;
31
39
  //# sourceMappingURL=constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,IAAI,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAE7F;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAG,OAAO,qBAAyB,CAAC;AAEjE;;;;GAIG;AACH,eAAO,MAAM,SAAS,mBAAmB,CAAC;AAE1C;;;;GAIG;AACH,eAAO,MAAM,SAAS,kBAAkB,CAAC;AAEzC;;;GAGG;AACH,eAAO,MAAM,KAAK,gHAQR,CAAC;AAEX;;;;GAIG;AACH,eAAO,MAAM,WAAW,YAAY,CAAC;AAErC,eAAO,MAAM,WAAW,aAAa,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,IAAI,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAE7F;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAG,OAAO,qBAAyB,CAAC;AAEjE;;;;GAIG;AACH,eAAO,MAAM,SAAS,mBAAmB,CAAC;AAE1C;;;;GAIG;AACH,eAAO,MAAM,SAAS,kBAAkB,CAAC;AAEzC;;;GAGG;AACH,eAAO,MAAM,KAAK,gHAQR,CAAC;AAEX;;;;GAIG;AACH,eAAO,MAAM,WAAW,YAAY,CAAC;AAErC,eAAO,MAAM,WAAW,aAAa,CAAC;AAEtC;;;;;GAKG;AACH,eAAO,MAAM,+BAA+B,MAAM,CAAC;AAEnD,eAAO,MAAM,yBAAyB,QAAY,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AASjE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAcnC,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAiBlD;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CAC/B,MAAM,EAAE,MAAM,EACd,aAAa,CAAC,EAAE,cAAc,EAC9B,MAAM,CAAC,EAAE,UAAU,GACnB,OAAO,CAAC,IAAI,CAAC,CA8GhB"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AASjE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAcnC,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAiBlD;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CAC/B,MAAM,EAAE,MAAM,EACd,aAAa,CAAC,EAAE,cAAc,EAC9B,MAAM,CAAC,EAAE,UAAU,GACnB,OAAO,CAAC,IAAI,CAAC,CAmHhB"}
package/dist/index.d.mts CHANGED
@@ -75,7 +75,38 @@ export type RenderPage = (event: IAppEvent, config: Config, ctx: {
75
75
  url: string;
76
76
  data: Record<string, any>;
77
77
  theme?: IThemeProvider;
78
+ /**
79
+ * Render the visitor's session: forward their access-token cookie
80
+ * (see `readRenderCookies`). Off, the page renders logged out.
81
+ */
82
+ session?: boolean;
78
83
  }) => Promise<string>;
84
+ export type SessionForwardingGuardOptions = {
85
+ /**
86
+ * Refused bearers per window before forwarding stops.
87
+ */
88
+ max?: number;
89
+ /**
90
+ * The window length in milliseconds.
91
+ */
92
+ window?: number;
93
+ /**
94
+ * The clock, a seam for tests.
95
+ */
96
+ now?: () => number;
97
+ };
98
+ export type SessionForwardingGuard = {
99
+ /**
100
+ * Reserve the right to forward the visitor's access token for one render.
101
+ * Resolves the function that ends the reservation once the render is done,
102
+ * or undefined when the limit is reached and the render must not forward.
103
+ */
104
+ reserve(): (() => void) | undefined;
105
+ /**
106
+ * Count the bearers the API refuses on this client.
107
+ */
108
+ observe(client: Client): Client;
109
+ };
79
110
  //#endregion
80
111
  //#region src/application.d.ts
81
112
  /**
@@ -158,6 +189,14 @@ export declare const PAGES: readonly ["/authorize", "/register", "/activate", "/
158
189
  */
159
190
  export declare const ASSETS_PATH = "/assets";
160
191
  export declare const HEALTH_PATH = "/healthy";
192
+ /**
193
+ * How many forwarded bearers the API may refuse within one window before a
194
+ * render stops forwarding the visitor's access token. Well below server-core's
195
+ * failed-authentication limit (300 a minute per address), so the console's
196
+ * address never reaches it, even across two unaligned windows.
197
+ */
198
+ export declare const SESSION_FORWARDING_REJECTED_MAX = 100;
199
+ export declare const SESSION_FORWARDING_WINDOW: number;
161
200
  //#endregion
162
201
  //#region src/handler.d.ts
163
202
  /**
@@ -173,9 +212,10 @@ export declare function createHandler(config: Config, themeProvider?: IThemeProv
173
212
  //#endregion
174
213
  //#region src/payload.d.ts
175
214
  /**
176
- * The service hydrates ANONYMOUSLY. It holds no credential of its own and
177
- * asks server-core only for what an unauthenticated visitor may see, which
178
- * is the whole of what these pages render.
215
+ * The service holds no credential of its own. Its page data is what an
216
+ * unauthenticated visitor may see; the only authenticated calls are the
217
+ * render's own, made with the visitor's forwarded access token (see
218
+ * `readRenderCookies`) through a client built per render.
179
219
  *
180
220
  * It dispatches against `apiInternalUrl`, never the browser-facing `apiUrl`:
181
221
  * this is a server-side call, so it must not depend on the public address
@@ -264,6 +304,17 @@ export declare function readDeviceFederatedLogin(event: IAppEvent): {
264
304
  * so nothing request-derived is rendered.
265
305
  */
266
306
  export declare function readDeviceError(event: IAppEvent): 'access_denied' | undefined;
307
+ /**
308
+ * The request's cookies a render may use to show the visitor's own session:
309
+ * the kit's access token and nothing else.
310
+ *
311
+ * The refresh token is deliberately NOT forwarded. A server render can not
312
+ * hand a rotated pair back to the browser, and refresh tokens rotate strictly,
313
+ * so a refresh here would leave the browser replaying a spent token into
314
+ * family revocation. Without it, a token that no longer verifies simply
315
+ * renders the logged-out page and the browser renews it on hydration.
316
+ */
317
+ export declare function readRenderCookies(event: IAppEvent): Record<string, string>;
267
318
  //#endregion
268
319
  //#region src/redirect.d.ts
269
320
  /**
@@ -318,4 +369,24 @@ export declare function resolveDistPath(distPath?: string): string | undefined;
318
369
  */
319
370
  export declare function createServer(config: Config): Promise<App>;
320
371
  //#endregion
372
+ //#region src/session.d.ts
373
+ /**
374
+ * Bounds the bearers a render forwards that the API refuses.
375
+ *
376
+ * A render resolves the visitor's session with their forwarded access token,
377
+ * and on a split deployment every such call reaches the API from this
378
+ * service's one address. server-core counts a refused bearer against the
379
+ * address it came from, so without a bound anyone sending junk access-token
380
+ * cookies would push the console's address over the API's failed
381
+ * authentication limit and lock every hosted page out of the API.
382
+ *
383
+ * A render reserves capacity before it forwards and returns it once it is
384
+ * done, so renders still waiting for the API count against the limit as well:
385
+ * otherwise a burst of parallel renders would all forward before the first
386
+ * refusal came back. Past the limit a page renders without the cookie, which
387
+ * is what a token that does not verify renders anyway; the browser restores
388
+ * the session on hydration.
389
+ */
390
+ export declare function createSessionForwardingGuard(options?: SessionForwardingGuardOptions): SessionForwardingGuard;
391
+ //#endregion
321
392
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/application.ts","../src/config.ts","../src/constants.ts","../src/handler.ts","../src/payload.ts","../src/redirect.ts","../src/render.ts","../src/resolve.ts","../src/server.ts"],"mappings":";;;;;;;;;;;;;;;;YA2BY,cAAc,gBACtB;GAEK,YAAY,QAAQ;GAEpB,YAAY,OAAO,KAAK;IAE7B;;;;;;YAQQ;;;;;;EAMR;;;;;;;;EAQA;;;;;;;;;;;;;;;EAeA;;;;;EAKA;EACA;;;;;EAKA;EAEA,OAAO;;;;;;;YAQC,cACR,OAAO,WACP,QAAQ,QACR;EACI;EACA,MAAM;EACN,QAAQ;MAEX;;;;;;;;;;;;;;;wBC1EW,kBACZ,UAAS,QAAQ,KAAK,yBAAyB,6BAC/C;;;;;;;;;;;;;;;;qBCWS,mDAAa,uCAAA,gDAAA;;QAAb,KAAA;oCAcX;;;;;;;;;;;wBAYoB,cAClB,OAAO,QAAQ,eACf,QAAQ;;;;;;wBAmCU,qBAAsB,QAAQ;;;;;;;qBCzFvC,yBAA0B;;;;;;qBAO1B;;;;;;qBAOA;;;;;qBAMA;;;;;;qBAeA;qBAEA;;;;;;;;;;;;wBCMS,cAClB,QAAQ,QACR,gBAAgB,gBAChB,SAAS,aACT,QAAQ;;;;;;;;;;;;;;;wBChCI,gBAAgB,QAAQ,SAAU;;;;;;;;;;;;;;;;;;;;wBAiDlC,qBAAqB,QAAQ,eAAgB,QAAQ;;;;;;wBAmC/C,kBAAkB,QAAQ,QAAQ,OAAO,YAAa,QAAQ;YAUxE;EAIR;EACA;;;;;;;;wBASY,sBACZ,OAAO,WACP,UAAU,wBACV,UAAS,sBACT;;;;;;;;;;wBA6BY,kBAAkB,OAAO;;;;;;;;;;wBA0BzB,oBAAoB,OAAO;;;;;;;wBAe3B,mBAAmB,OAAO;;;;;;;;wBAqB1B,yBAAyB,OAAO;EAAe;;;;;;wBAY/C,gBAAgB,OAAO;;;;;;;;;;;;;;;wBChOvB,yBAAyB;;;;;;;;;wBCanB,qBAAqB,mBAAoB;wBAc/C,iBAAiB,oBAAqB;;;;;;;;;;;;;;;wBCtBtC,mBAAmB;;;;;;wBAYnB,gBAAgB;;;;;;;wBCrBV,aAAa,QAAQ,SAAU,QAAQ"}
1
+ {"version":3,"file":"index.d.mts","names":["BundleContractVersion"],"sources":["../src/types.ts","../src/application.ts","../src/config.ts","../src/constants.ts","../src/handler.ts","../src/payload.ts","../src/redirect.ts","../src/render.ts","../src/resolve.ts","../src/server.ts","../src/session.ts"],"mappings":";;;;;;;;;;;;;;;;YA4BY,cAAc,gBACtB;GAEK,YAAY,QAAQ;GAEpB,YAAY,OAAO,KAAK;IAE7B;;;;;;YAQQ;;;;;;EAMR;;;;;;;;EAQA;;;;;;;;;;;;;;;EAeA;;;;;EAKA;EACA;;;;;EAKA;EAEA,OAAO;;;;;;;YAQC,cACR,OAAO,WACP,QAAQ,QACR;EACI;EACA,MAAM;EACN,QAAQ;;;;;EAKR;MAEH;YAEO;;;;EAIR;;;;EAIA;;;;EAIA;;YAGQ;;;;;;EAMR;;;;EAIA,QAAQ,QAAQ,SAAU;;;;;;;;;;;;;;;;wBC3Gd,kBACZ,UAAS,QAAQ,KAAK,yBAAyB,6BAC/C;;;;;;;;;;;;;;;;qBCWS,mDAAa,uCAAA,gDAAA;;QAAb,KAAA;oCAcX;;;;;;;;;;;wBAYoB,cAClB,OAAO,QAAQ,eACf,QAAQ;;;;;;wBAmCU,qBAAsB,QAAQ;;;;;;;qBCzFvC,yBAA0BA;;;;;;qBAO1B;;;;;;qBAOA;;;;;qBAMA;;;;;;qBAeA;qBAEA;;;;;;;qBAQA;qBAEA;;;;;;;;;;;;wBCJS,cAClB,QAAQ,QACR,gBAAgB,gBAChB,SAAS,aACT,QAAQ;;;;;;;;;;;;;;;;wBC9BI,gBAAgB,QAAQ,SAAU;;;;;;;;;;;;;;;;;;;;wBAiDlC,qBAAqB,QAAQ,eAAgB,QAAQ;;;;;;wBAmC/C,kBAAkB,QAAQ,QAAQ,OAAO,YAAa,QAAQ;YAUxE;EAIR;EACA;;;;;;;;wBASY,sBACZ,OAAO,WACP,UAAU,wBACV,UAAS,sBACT;;;;;;;;;;wBA6BY,kBAAkB,OAAO;;;;;;;;;;wBA0BzB,oBAAoB,OAAO;;;;;;;wBAe3B,mBAAmB,OAAO;;;;;;;;wBAqB1B,yBAAyB,OAAO;EAAe;;;;;;wBAY/C,gBAAgB,OAAO;;;;;;;;;;;wBAcvB,kBAAkB,OAAO,YAAa;;;;;;;;;;;;;;;wBChPtC,yBAAyB;;;;;;;;;wBCmBnB,qBAAqB,mBAAoB;wBAc/C,iBAAiB,oBAAqB;;;;;;;;;;;;;;;wBC5BtC,mBAAmB;;;;;;wBAYnB,gBAAgB;;;;;;;wBCrBV,aAAa,QAAQ,SAAU,QAAQ;;;;;;;;;;;;;;;;;;;;wBC+B7C,6BACZ,UAAS,gCACT"}
package/dist/index.d.ts CHANGED
@@ -7,5 +7,6 @@ export * from './redirect';
7
7
  export * from './render';
8
8
  export * from './resolve';
9
9
  export * from './server';
10
+ export * from './session';
10
11
  export * from './types';
11
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC"}
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { C as CONFIG_SCHEMA, S as VITE_BASE, T as resolveConfig, _ as ASSETS_PATH, a as resolveDistPath, b as HEALTH_PATH, c as createAPIClient, d as readDeviceError, f as readDeviceFederatedLogin, g as sanitizeRelativeRedirect, h as readUILocalesHint, i as createRenderPage, l as createFeaturesReader, m as readUIColorModeHint, n as createHandler, o as resolvePackagePath, p as readDeviceUserCode, r as assertRenderContract, s as buildWorkflowPageData, t as createApplication, u as readAuthorizeInfo, v as BASE_PATH, w as readConfigFromEnv, x as PAGES, y as CONTRACT_VERSION } from "./application-h8KbQSdz.mjs";
1
+ import { C as PAGES, D as CONFIG_SCHEMA, E as VITE_BASE, O as readConfigFromEnv, S as HEALTH_PATH, T as SESSION_FORWARDING_WINDOW, _ as readUILocalesHint, a as createSessionForwardingGuard, b as BASE_PATH, c as buildWorkflowPageData, d as readAuthorizeInfo, f as readDeviceError, g as readUIColorModeHint, h as readRenderCookies, i as createRenderPage, k as resolveConfig, l as createAPIClient, m as readDeviceUserCode, n as createHandler, o as resolveDistPath, p as readDeviceFederatedLogin, r as assertRenderContract, s as resolvePackagePath, t as createApplication, u as createFeaturesReader, v as sanitizeRelativeRedirect, w as SESSION_FORWARDING_REJECTED_MAX, x as CONTRACT_VERSION, y as ASSETS_PATH } from "./application-DvnUIu4h.mjs";
2
2
  import { App } from "routup";
3
3
  //#region src/server.ts
4
4
  /**
@@ -11,6 +11,6 @@ async function createServer(config) {
11
11
  return app;
12
12
  }
13
13
  //#endregion
14
- export { ASSETS_PATH, BASE_PATH, CONFIG_SCHEMA, CONTRACT_VERSION, HEALTH_PATH, PAGES, VITE_BASE, assertRenderContract, buildWorkflowPageData, createAPIClient, createApplication, createFeaturesReader, createHandler, createRenderPage, createServer, readAuthorizeInfo, readConfigFromEnv, readDeviceError, readDeviceFederatedLogin, readDeviceUserCode, readUIColorModeHint, readUILocalesHint, resolveConfig, resolveDistPath, resolvePackagePath, sanitizeRelativeRedirect };
14
+ export { ASSETS_PATH, BASE_PATH, CONFIG_SCHEMA, CONTRACT_VERSION, HEALTH_PATH, PAGES, SESSION_FORWARDING_REJECTED_MAX, SESSION_FORWARDING_WINDOW, VITE_BASE, assertRenderContract, buildWorkflowPageData, createAPIClient, createApplication, createFeaturesReader, createHandler, createRenderPage, createServer, createSessionForwardingGuard, readAuthorizeInfo, readConfigFromEnv, readDeviceError, readDeviceFederatedLogin, readDeviceUserCode, readRenderCookies, readUIColorModeHint, readUILocalesHint, resolveConfig, resolveDistPath, resolvePackagePath, sanitizeRelativeRedirect };
15
15
 
16
16
  //# sourceMappingURL=index.mjs.map
package/dist/payload.d.ts CHANGED
@@ -3,9 +3,10 @@ import { Client } from '@authup/core-http-kit';
3
3
  import type { IAppEvent } from 'routup';
4
4
  import type { Config } from './types';
5
5
  /**
6
- * The service hydrates ANONYMOUSLY. It holds no credential of its own and
7
- * asks server-core only for what an unauthenticated visitor may see, which
8
- * is the whole of what these pages render.
6
+ * The service holds no credential of its own. Its page data is what an
7
+ * unauthenticated visitor may see; the only authenticated calls are the
8
+ * render's own, made with the visitor's forwarded access token (see
9
+ * `readRenderCookies`) through a client built per render.
9
10
  *
10
11
  * It dispatches against `apiInternalUrl`, never the browser-facing `apiUrl`:
11
12
  * this is a server-side call, so it must not depend on the public address
@@ -94,4 +95,15 @@ export declare function readDeviceFederatedLogin(event: IAppEvent): {
94
95
  * so nothing request-derived is rendered.
95
96
  */
96
97
  export declare function readDeviceError(event: IAppEvent): 'access_denied' | undefined;
98
+ /**
99
+ * The request's cookies a render may use to show the visitor's own session:
100
+ * the kit's access token and nothing else.
101
+ *
102
+ * The refresh token is deliberately NOT forwarded. A server render can not
103
+ * hand a rotated pair back to the browser, and refresh tokens rotate strictly,
104
+ * so a refresh here would leave the browser replaying a spent token into
105
+ * family revocation. Without it, a token that no longer verifies simply
106
+ * renders the logged-out page and the browser renews it on hydration.
107
+ */
108
+ export declare function readRenderCookies(event: IAppEvent): Record<string, string>;
97
109
  //# sourceMappingURL=payload.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"payload.d.ts","sourceRoot":"","sources":["../src/payload.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAkB,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACnG,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAI/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAExC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEtC;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAI,MAAM,CAEvD;AA4BD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAI,MAAM,OAAO,CAAC,sBAAsB,CAAC,CA4B3F;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAI,OAAO,CAAC,aAAa,CAAC,CAQjG;AAED,MAAM,MAAM,mBAAmB,GAAG;IAI9B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACjC,KAAK,EAAE,SAAS,EAChB,QAAQ,EAAE,sBAAsB,EAChC,OAAO,GAAE,mBAAwB,GACjC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAkBtB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,GAAI,MAAM,GAAG,SAAS,CAevE;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,SAAS,GAAI,MAAM,GAAG,SAAS,CAOzE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,GAAI,MAAM,GAAG,SAAS,CAYxE;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,SAAS,GAAI;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAM9F;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,GAAI,eAAe,GAAG,SAAS,CAE9E"}
1
+ {"version":3,"file":"payload.d.ts","sourceRoot":"","sources":["../src/payload.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAkB,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACnG,OAAO,EAAE,MAAM,EAAc,MAAM,uBAAuB,CAAC;AAK3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAExC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEtC;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAI,MAAM,CAEvD;AA4BD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAI,MAAM,OAAO,CAAC,sBAAsB,CAAC,CA4B3F;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAI,OAAO,CAAC,aAAa,CAAC,CAQjG;AAED,MAAM,MAAM,mBAAmB,GAAG;IAI9B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACjC,KAAK,EAAE,SAAS,EAChB,QAAQ,EAAE,sBAAsB,EAChC,OAAO,GAAE,mBAAwB,GACjC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAkBtB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,GAAI,MAAM,GAAG,SAAS,CAevE;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,SAAS,GAAI,MAAM,GAAG,SAAS,CAOzE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,GAAI,MAAM,GAAG,SAAS,CAYxE;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,SAAS,GAAI;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAM9F;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,GAAI,eAAe,GAAG,SAAS,CAE9E;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,GAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAI3E"}
@@ -1 +1 @@
1
- {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C;;;;;GAKG;AACH,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAI,OAAO,CAAC,IAAI,CAAC,CAY3E;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAI,UAAU,CAoG/D"}
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C;;;;;GAKG;AACH,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAI,OAAO,CAAC,IAAI,CAAC,CAY3E;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAI,UAAU,CAyH/D"}
@@ -0,0 +1,20 @@
1
+ import type { SessionForwardingGuard, SessionForwardingGuardOptions } from './types';
2
+ /**
3
+ * Bounds the bearers a render forwards that the API refuses.
4
+ *
5
+ * A render resolves the visitor's session with their forwarded access token,
6
+ * and on a split deployment every such call reaches the API from this
7
+ * service's one address. server-core counts a refused bearer against the
8
+ * address it came from, so without a bound anyone sending junk access-token
9
+ * cookies would push the console's address over the API's failed
10
+ * authentication limit and lock every hosted page out of the API.
11
+ *
12
+ * A render reserves capacity before it forwards and returns it once it is
13
+ * done, so renders still waiting for the API count against the limit as well:
14
+ * otherwise a burst of parallel renders would all forward before the first
15
+ * refusal came back. Past the limit a page renders without the cookie, which
16
+ * is what a token that does not verify renders anyway; the browser restores
17
+ * the session on hydration.
18
+ */
19
+ export declare function createSessionForwardingGuard(options?: SessionForwardingGuardOptions): SessionForwardingGuard;
20
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,sBAAsB,EAAE,6BAA6B,EAAE,MAAM,SAAS,CAAC;AAiBrF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,6BAAkC,GAC3C,sBAAsB,CAiDzB"}
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Client } from '@authup/core-http-kit';
1
2
  import type { AuthConsoleConfig, CoreConfig, RootConfig, SECTION_KEY, ThemeConfig, ToObjectLiteral } from '@authup/server-config';
2
3
  import type { IThemeProvider } from '@authup/server-console-kit';
3
4
  import type { IAppEvent } from 'routup';
@@ -71,5 +72,36 @@ export type RenderPage = (event: IAppEvent, config: Config, ctx: {
71
72
  url: string;
72
73
  data: Record<string, any>;
73
74
  theme?: IThemeProvider;
75
+ /**
76
+ * Render the visitor's session: forward their access-token cookie
77
+ * (see `readRenderCookies`). Off, the page renders logged out.
78
+ */
79
+ session?: boolean;
74
80
  }) => Promise<string>;
81
+ export type SessionForwardingGuardOptions = {
82
+ /**
83
+ * Refused bearers per window before forwarding stops.
84
+ */
85
+ max?: number;
86
+ /**
87
+ * The window length in milliseconds.
88
+ */
89
+ window?: number;
90
+ /**
91
+ * The clock, a seam for tests.
92
+ */
93
+ now?: () => number;
94
+ };
95
+ export type SessionForwardingGuard = {
96
+ /**
97
+ * Reserve the right to forward the visitor's access token for one render.
98
+ * Resolves the function that ends the reservation once the render is done,
99
+ * or undefined when the limit is reached and the render must not forward.
100
+ */
101
+ reserve(): (() => void) | undefined;
102
+ /**
103
+ * Count the bearers the API refuses on this client.
104
+ */
105
+ observe(client: Client): Client;
106
+ };
75
107
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACR,iBAAiB,EACjB,UAAU,EACV,UAAU,EACV,WAAW,EACX,WAAW,EACX,eAAe,EAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAExC;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,GAAG,eAAe,CACrC,UAAU,GACV;IACI,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;IAEjC,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC,CAAA;CACxD,GACD,iBAAiB,CACpB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,MAAM,GAAG;IACjB;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;;;;;;OAaG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB,KAAK,EAAE,WAAW,CAAA;CACrB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,CACrB,KAAK,EAAE,SAAS,EAChB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE;IACD,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC1B,KAAK,CAAC,EAAE,cAAc,CAAC;CAC1B,KACA,OAAO,CAAC,MAAM,CAAC,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,EACR,iBAAiB,EACjB,UAAU,EACV,UAAU,EACV,WAAW,EACX,WAAW,EACX,eAAe,EAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAExC;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,GAAG,eAAe,CACrC,UAAU,GACV;IACI,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;IAEjC,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC,CAAA;CACxD,GACD,iBAAiB,CACpB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,MAAM,GAAG;IACjB;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;;;;;;OAaG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB,KAAK,EAAE,WAAW,CAAA;CACrB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,CACrB,KAAK,EAAE,SAAS,EAChB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE;IACD,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC1B,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB,KACA,OAAO,CAAC,MAAM,CAAC,CAAC;AAErB,MAAM,MAAM,6BAA6B,GAAG;IACxC;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACjC;;;;OAIG;IACH,OAAO,IAAK,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IACrC;;OAEG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,GAAI,MAAM,CAAC;CACpC,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@authup/server-auth-console",
3
3
  "type": "module",
4
- "version": "1.0.0-beta.67",
4
+ "version": "1.0.0-beta.69",
5
5
  "description": "The SSR service behind authup's hosted auth pages: it renders the auth console bundle and hydrates it anonymously over HTTP.",
6
6
  "license": "AGPL-3.0-only",
7
7
  "bin": {
@@ -51,16 +51,16 @@
51
51
  },
52
52
  "homepage": "https://github.com/authup/authup#readme",
53
53
  "dependencies": {
54
- "@authup/client-auth-console": "^1.0.0-beta.67",
55
- "@authup/core-http-kit": "^1.0.0-beta.67",
56
- "@authup/errors": "^1.0.0-beta.67",
57
- "@authup/kit": "^1.0.0-beta.67",
58
- "@authup/server-config": "^1.0.0-beta.67",
59
- "@authup/server-config-kit": "^1.0.0-beta.67",
60
- "@authup/server-console-kit": "^1.0.0-beta.67",
61
- "@authup/specs": "^1.0.0-beta.67",
54
+ "@authup/client-auth-console": "^1.0.0-beta.69",
55
+ "@authup/core-http-kit": "^1.0.0-beta.69",
56
+ "@authup/errors": "^1.0.0-beta.69",
57
+ "@authup/kit": "^1.0.0-beta.69",
58
+ "@authup/server-config": "^1.0.0-beta.69",
59
+ "@authup/server-config-kit": "^1.0.0-beta.69",
60
+ "@authup/server-console-kit": "^1.0.0-beta.69",
61
+ "@authup/specs": "^1.0.0-beta.69",
62
62
  "@routup/assets": "^5.0.0",
63
- "@routup/basic": "^3.0.0",
63
+ "@routup/basic": "^3.0.1",
64
64
  "locter": "^4.1.1",
65
65
  "orkos": "^1.1.1",
66
66
  "routup": "^6.1.0",
@@ -68,9 +68,9 @@
68
68
  "zod": "^4.6.5"
69
69
  },
70
70
  "devDependencies": {
71
- "@types/node": "^26.6.1",
71
+ "@types/node": "^26.6.5",
72
72
  "cross-env": "^10.1.0",
73
- "vitest": "^4.0.18"
73
+ "vitest": "^5.0.3"
74
74
  },
75
75
  "publishConfig": {
76
76
  "access": "public"
@@ -1 +0,0 @@
1
- {"version":3,"file":"application-h8KbQSdz.mjs","names":["createHandler","createAssetsHandler","createApplication","createConsoleApplication","createHandler"],"sources":["../src/config.ts","../src/constants.ts","../src/redirect.ts","../src/payload.ts","../src/path.ts","../src/resolve.ts","../src/render.ts","../src/handler.ts","../src/application.ts"],"sourcesContent":["/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport {\n AUTH_CONSOLE_SCHEMA,\n CORE_SCHEMA,\n ROOT_SCHEMA,\n SECTION_KEY,\n THEME_SCHEMA,\n} from '@authup/server-config';\nimport {\n buildSchemaDefaults,\n defineSchema,\n mergeSchemaData,\n mountSchema,\n readSchemaFromEnv,\n resolveSchemaData,\n} from '@authup/server-config-kit';\nimport { Container } from 'validup';\nimport type { CoreConfig, EnvironmentVariable } from '@authup/server-config';\nimport type { Config, ConfigInput } from './types';\n\n/**\n * The keys this service reads, SELECTED out of the document schema by name.\n *\n * Nothing is declared here: every key of `authup.yml` is declared once in\n * `@authup/server-config`, so this service cannot spell a path, an\n * environment variable, a default or a reader differently from server-core,\n * which reads several of the same keys. Neither package depends on the other;\n * both depend on the declaration.\n *\n * Its own section is spread, so this service reads it in its own vocabulary\n * (`url`, `port`, `host`); every other section stays under the key the\n * document nests it at.\n */\nexport const CONFIG_SCHEMA = defineSchema<ConfigInput, 'publicUrl' | 'internalUrl' | 'db'>({\n ...AUTH_CONSOLE_SCHEMA,\n ...ROOT_SCHEMA,\n [SECTION_KEY.THEME]: THEME_SCHEMA,\n // Two keys of core, never the section. `publicUrl` derives from\n // the core listener address, which is what a console needs; selecting the\n // whole section also selects its INVARIANTS, and `resolveSchemaData` runs\n // every resolver in a registry -- so a console would refuse to start on a\n // short SECRETS_ENCRYPTION_KEY, or on mfaRequired without mfaEnabled, for\n // keys it has no database, key store or MFA to use.\n [SECTION_KEY.CORE]: defineSchema<Pick<CoreConfig, 'host' | 'port'>, never, EnvironmentVariable>({\n host: CORE_SCHEMA.host,\n port: CORE_SCHEMA.port,\n }),\n});\n\n/**\n * Turn the configuration namespace into the service's own shape: validate the\n * input against the declared zod types, fill the defaults, derive the one key\n * that is derived rather than configured, and rename. An empty `url` means the console sits on server-core's own origin\n * under the default segment, which is the single-origin deployment.\n *\n * The defaults are layered SECTION-AWARE ({@link mergeSchemaData}): a spread\n * would let an input carrying one key of a section replace the whole section\n * and take every other key's default with it.\n */\nexport async function resolveConfig(\n input: Partial<ConfigInput>,\n) : Promise<Config> {\n const validator = new Container<ConfigInput>();\n mountSchema(validator, CONFIG_SCHEMA);\n\n const parsed = await validator.run(input);\n\n const values = resolveSchemaData<ConfigInput>(\n CONFIG_SCHEMA,\n mergeSchemaData<ConfigInput>(\n CONFIG_SCHEMA,\n buildSchemaDefaults<ConfigInput>(CONFIG_SCHEMA),\n parsed,\n ),\n ) as ConfigInput;\n\n return {\n url: values.url,\n apiUrl: values.publicUrl,\n // `internalUrl` falls back to `publicUrl`, so an operator who has one\n // address for both sides configures nothing. The composed `authup\n // start` overrides this with its own listen address unless the\n // document named one.\n apiInternalUrl: values.internalUrl,\n port: values.port,\n host: values.host,\n distPath: values.path,\n theme: values.theme,\n };\n}\n\n/**\n * The standalone entry's configuration, read from the environment alone.\n * `authup.yml` reaches this service through the CLI roles, which compose this\n * very registry into the one document loader.\n */\nexport async function readConfigFromEnv() : Promise<Config> {\n // The explicit type argument is load-bearing: inferred from the schema\n // object, a key declared without a default (the derived publicUrl) comes\n // back as unknown.\n const input : Partial<ConfigInput> = readSchemaFromEnv<ConfigInput>(CONFIG_SCHEMA);\n\n return resolveConfig(input);\n}\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport type { CONTRACT_VERSION as BundleContractVersion } from '@authup/client-auth-console';\n\n/**\n * The render contract this service compiles against. The literal type fails\n * the build the moment the bundle's constant moves without this one.\n */\nexport const CONTRACT_VERSION : typeof BundleContractVersion = 5;\n\n/**\n * The vite base the auth console bundle is built with. Asset hrefs in the\n * shell are emitted against it, so the service mounts its assets there and\n * rebases them only when the deployment carries a sub-path.\n */\nexport const VITE_BASE = '/console/auth/';\n\n/**\n * The path the service is served under by default, i.e. the path component\n * of `authConsole.url`. It is the same segment server-core mounted\n * the assets at before the split, so a built bundle needs no rebuild.\n */\nexport const BASE_PATH = '/console/auth';\n\n/**\n * The pages the service renders. Each is a route of the console bundle's\n * own router, and each is what a server-core page GET now redirects to.\n */\nexport const PAGES = [\n '/authorize',\n '/register',\n '/activate',\n '/password-forgot',\n '/password-reset',\n '/logout',\n '/device',\n] as const;\n\n/**\n * Where this service mounts the bundle's `assets/` directory. The public\n * asset URL is the service's own public path plus this route, which is what\n * the bundle's fixed vite base is rebased onto.\n */\nexport const ASSETS_PATH = '/assets';\n\nexport const HEALTH_PATH = '/healthy';\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\n/**\n * Open-redirect guard for the `redirect` query parameter carried by the\n * auth workflow pages: only same-origin relative paths pass through, so the\n * value can be safely reflected into an anchor href / forwarded as a\n * post-login destination.\n *\n * Rejects: non-strings, control/whitespace chars (browsers strip some,\n * shifting where the URL authority begins), protocol-relative (`//host`),\n * backslash variants (browsers normalise `\\` → `/`), and any scheme\n * (`javascript:`, `http:`) — both on the raw and once-decoded form so a\n * percent-encoded separator (`%2f`, `%5c`) can't slip past.\n */\nexport function sanitizeRelativeRedirect(input: unknown): string | undefined {\n if (typeof input !== 'string' || input.length === 0) {\n return undefined;\n }\n\n // Raw input: reject control chars AND whitespace (space/tab/newline) — a\n // raw space can shift where the URL authority begins.\n // eslint-disable-next-line no-control-regex\n if (/[\\u0000-\\u0020\\u007f]/.test(input)) {\n return undefined;\n }\n\n let decoded: string;\n try {\n decoded = decodeURIComponent(input);\n } catch {\n return undefined;\n }\n\n // Decoded form: reject C0 controls + DEL (CR/LF/NUL — the header-injection\n // vectors a `%0a`/`%0d` would smuggle past the raw check) but NOT space\n // (0x20), so a legitimate `%20` in the path still passes.\n // eslint-disable-next-line no-control-regex\n if (/[\\u0000-\\u001f\\u007f]/.test(decoded)) {\n return undefined;\n }\n\n for (const candidate of [input, decoded]) {\n if (!candidate.startsWith('/')) {\n return undefined;\n }\n if (candidate.length > 1 && (candidate[1] === '/' || candidate[1] === '\\\\')) {\n return undefined;\n }\n if (candidate.includes('\\\\')) {\n return undefined;\n }\n }\n\n return input;\n}\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport type { AuthorizeInfo, StatusResponse, StatusResponseFeatures } from '@authup/core-http-kit';\nimport { Client } from '@authup/core-http-kit';\nimport { isUUID } from '@authup/kit';\nimport { OAuth2UIColorMode } from '@authup/specs';\nimport { useRequestQuery } from '@routup/basic/query';\nimport type { IAppEvent } from 'routup';\nimport { sanitizeRelativeRedirect } from './redirect';\nimport type { Config } from './types';\n\n/**\n * The service hydrates ANONYMOUSLY. It holds no credential of its own and\n * asks server-core only for what an unauthenticated visitor may see, which\n * is the whole of what these pages render.\n *\n * It dispatches against `apiInternalUrl`, never the browser-facing `apiUrl`:\n * this is a server-side call, so it must not depend on the public address\n * resolving from inside the deployment. Unlike server-core's own internal\n * client the swap is on `baseURL` rather than at the transport layer,\n * because nothing derived from it reaches a caller -- there is no\n * `redirect_uri` here to be compared byte for byte later.\n */\nexport function createAPIClient(config: Config) : Client {\n return new Client({ baseURL: config.apiInternalUrl });\n}\n\n/**\n * Report an outbound failure to the operator and raise one the visitor may\n * see, which is deliberately not the same error.\n *\n * hapic embeds the resolved request URL in its message, and this service has\n * no error middleware: routup answers with `event.error.message` verbatim, on\n * the public login page. That URL is `apiInternalUrl`, which on a split\n * deployment names a service on the operator's own network, so an API that is\n * merely down would publish its internal address to every anonymous visitor.\n * It was harmless while the value was `publicUrl`, an address the browser\n * already had.\n *\n * The rule and the remedy are server-core's own (`sanitizeError` /\n * `describeError`): the caller gets a message it can act on, the detail goes\n * to the log. `console.error` rather than a logger because a console handler\n * is built from a config alone and is given none -- and because this line is\n * the only trace such a failure leaves, which is how #3550 was diagnosed in\n * the first place.\n */\nfunction failWithoutAddress(error: unknown) : never {\n // eslint-disable-next-line no-console\n console.error('[authup] the auth console could not reach the API:', error);\n\n throw new Error('The identity provider could not be reached.');\n}\n\n/**\n * The feature flags every workflow page gates its form on. A page that\n * cannot reach them must not silently claim a workflow is disabled, so the\n * failure propagates rather than defaulting to false.\n *\n * Read ONCE and held for the reader's lifetime, the same shape render.ts\n * keeps for the dist template, manifest and render entry: `GET /` reports\n * the version and the boot-config feature flags, none of which changes\n * without a restart, so a TTL would be a number with nothing behind it.\n * Without it every workflow page render is an API call, and under the\n * composed `authup start` those all arrive over loopback on one shared\n * rate-limit key. Only a SUCCESS is held, so an API that was down at the\n * first render is retried rather than remembered as broken.\n *\n * The cache is instance-scoped rather than module-scoped (098 C4): the\n * answer depends on which API the client was built against, so two handlers\n * in one process must not share one, and a module slot would additionally\n * leak between test cases.\n */\nexport function createFeaturesReader(client: Client) : () => Promise<StatusResponseFeatures> {\n let cached : StatusResponseFeatures | undefined;\n let pending : Promise<StatusResponseFeatures> | undefined;\n\n return () => {\n if (cached) {\n return Promise.resolve(cached);\n }\n\n if (pending) {\n return pending;\n }\n\n pending = client.status.get()\n .then((status: StatusResponse) => {\n cached = status.features;\n\n return cached;\n })\n .catch((e) => failWithoutAddress(e));\n\n const clear = () => {\n pending = undefined;\n };\n pending.then(clear, clear);\n\n return pending;\n };\n}\n\n/**\n * The `/authorize` render input, resolved by server-core (plan 101 D2-1).\n * The browser's query is forwarded verbatim: the answer is derived from all\n * of it, the `provider` hint and the closed `error` marker set included.\n */\nexport async function readAuthorizeInfo(client: Client, event: IAppEvent) : Promise<AuthorizeInfo> {\n const { search } = new URL(event.request.url);\n\n try {\n return await client.authorize.getInfo(search);\n } catch (e) {\n return failWithoutAddress(e);\n }\n}\n\nexport type WorkflowPageOptions = {\n // Whether the page consumes the `realmId` (legacy `realm_id`) / `token`\n // query params (e.g. prefill from an email deep link). Off by default so\n // a page never reflects a param it doesn't use.\n realmAware?: boolean,\n tokenAware?: boolean,\n};\n\n/**\n * The payload the four workflow pages render from: the feature flags plus\n * the handful of query params each one declares. Unchanged from the\n * assembly server-core did before the split, including the open-redirect\n * guard on `redirect`.\n */\nexport function buildWorkflowPageData(\n event: IAppEvent,\n features: StatusResponseFeatures,\n options: WorkflowPageOptions = {},\n) : Record<string, any> {\n const query = useRequestQuery(event);\n\n const data: Record<string, any> = {\n features,\n redirect: sanitizeRelativeRedirect(query.redirect),\n };\n\n if (options.realmAware) {\n const realmId = typeof query.realmId === 'string' ? query.realmId : query.realm_id;\n data.realmId = typeof realmId === 'string' ? realmId : undefined;\n }\n\n if (options.tokenAware) {\n data.token = typeof query.token === 'string' ? query.token : undefined;\n }\n\n return data;\n}\n\n/**\n * The RP's own UI language, from the OIDC Core section 3.1.2.1 `ui_locales`\n * parameter: a space-separated list of BCP47 tags in preference order.\n *\n * It rides the authorization request, so it survives the page GET's hop and\n * the rebuild a federated round-trip makes from the stored code request. The\n * four workflow pages take it straight from their own query, since an RP\n * linking to one can say the same thing there.\n */\nexport function readUILocalesHint(event: IAppEvent) : string | undefined {\n const { ui_locales: uiLocales } = useRequestQuery(event);\n if (typeof uiLocales !== 'string') {\n return undefined;\n }\n\n // ponytail: the first well-formed tag wins, and it is NOT narrowed to a\n // locale authup has a catalog for. That is the latitude the navigator\n // path already takes -- a `de-CH` visitor is stamped `lang=\"de-CH\"` and\n // reads the `de` catalog. Narrow here (matchLocale over LOCALES) if the\n // lang attribute ever has to name only an authored locale, which costs\n // this service a dependency on @authup/i18n.\n return uiLocales\n .split(' ')\n .find((tag) => /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]+)*$/.test(tag));\n}\n\n/**\n * The color mode the RP renders in, from authup's own `ui_color_mode`. One\n * value rather than a list, since there is nothing to negotiate.\n *\n * Checked against the enum rather than local literals because the realm's\n * discovery document ADVERTISES that same set as\n * `ui_color_modes_supported`: a second spelling here would let the pages and\n * the document disagree about what the deployment accepts.\n */\nexport function readUIColorModeHint(event: IAppEvent) : string | undefined {\n const { ui_color_mode: uiColorMode } = useRequestQuery(event);\n\n return typeof uiColorMode === 'string' &&\n (Object.values(OAuth2UIColorMode) as string[]).includes(uiColorMode) ?\n uiColorMode :\n undefined;\n}\n\n/**\n * The `user_code` the device page prefills (RFC 8628 section 3.3.1): the\n * query value uppercased and stripped of everything outside `A-Z0-9`, or\n * `undefined` when nothing bounded remains. The page renders it back, so what\n * reaches the payload is at most 16 alphanumeric characters.\n */\nexport function readDeviceUserCode(event: IAppEvent) : string | undefined {\n const query = useRequestQuery(event);\n if (typeof query.user_code !== 'string') {\n return undefined;\n }\n\n const canonical = query.user_code.toUpperCase().replace(/[^A-Z0-9]/g, '');\n if (canonical.length === 0 || canonical.length > 16) {\n return undefined;\n }\n\n return canonical;\n}\n\n/**\n * The provider a federated login on the device page came back from (#3589).\n * A routing hint and no secret: the pending login itself rides an HttpOnly\n * cookie. Anything that is not a uuid is dropped, the rule `/authorize`\n * reads its own hint by: the page interpolates the value into the\n * completion request path.\n */\nexport function readDeviceFederatedLogin(event: IAppEvent) : { providerId: string } | undefined {\n const { provider } = useRequestQuery(event);\n\n return typeof provider === 'string' && isUUID(provider) ?\n { providerId: provider } :\n undefined;\n}\n\n/**\n * The refusal marker the federated callback may attach. A closed set of one,\n * so nothing request-derived is rendered.\n */\nexport function readDeviceError(event: IAppEvent) : 'access_denied' | undefined {\n return useRequestQuery(event).error === 'access_denied' ? 'access_denied' : undefined;\n}\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nexport const CODE_PATH = path.dirname(fileURLToPath(import.meta.url));\nexport const PACKAGE_PATH = path.join(CODE_PATH, '..');\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport { locateUpSync } from 'locter';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { PACKAGE_PATH } from './path';\n\n/**\n * Locate the auth console package: a substituted one (config `distPath`)\n * first, else the SSR auth workflow UI shipped as\n * `@authup/client-auth-console` (a dependency of THIS service), which the\n * node_modules ancestor walk from this package's own root (locter's\n * `locateUp`) finds for the workspace (symlink onto\n * apps/client-auth-console) and for a published install alike.\n *\n * The `cwd` anchor is behavioural, not incidental: it decides which\n * node_modules tree is walked, so it stays pinned to this package rather\n * than to the process cwd.\n */\nexport function resolvePackagePath(distPath?: string) : string | undefined {\n return distPath || locateUpSync(\n 'node_modules/@authup/client-auth-console/package.json',\n { cwd: PACKAGE_PATH },\n )?.directory;\n}\n\n/**\n * Locate the BUILT auth console bundle (`dist/client` template + assets,\n * `dist/server/server.js` render entry). A half-built package is no bundle:\n * without the render entry there is nothing to render with.\n */\nexport function resolveDistPath(distPath?: string) : string | undefined {\n const packagePath = resolvePackagePath(distPath);\n if (!packagePath) {\n return undefined;\n }\n\n const candidate = path.join(packagePath, 'dist');\n if (\n fs.existsSync(path.join(candidate, 'client', 'index.html')) &&\n fs.existsSync(path.join(candidate, 'server', 'server.js'))\n ) {\n return candidate;\n }\n\n return undefined;\n}\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport type { HydrationPayload, RenderFunction } from '@authup/client-auth-console';\nimport { InternalError } from '@authup/errors';\nimport { getURLBasePath } from '@authup/kit';\nimport {\n applyTheme,\n applyUIPageHeaders,\n readUIClientPreferences,\n rebaseAssetURLs,\n replaceTemplateMarker,\n stampHtmlAttributes,\n} from '@authup/server-console-kit';\nimport { read } from 'locter';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { CONTRACT_VERSION, VITE_BASE } from './constants';\nimport { readUIColorModeHint, readUILocalesHint } from './payload';\nimport { resolveDistPath } from './resolve';\nimport type { RenderPage } from './types';\n\n/**\n * Refuse a bundle built against another render contract at boot, naming the\n * entry and both versions, instead of failing per request on `/authorize`.\n * A bundle predating the export is version 1. Reads the module itself, which\n * Node's ESM cache then hands the render for free.\n */\nexport async function assertRenderContract(distPath: string) : Promise<void> {\n const entry = path.join(distPath, 'server', 'server.js');\n const bundle = await read(entry) as Record<string, any>;\n const version = bundle.CONTRACT_VERSION ?? 1;\n\n if (version !== CONTRACT_VERSION) {\n throw new InternalError(\n `The auth console bundle \"${entry}\" implements render-contract version ${version}, ` +\n `but this service requires ${CONTRACT_VERSION}. ` +\n 'Rebuild it against the current @authup/client-auth-console contract.',\n );\n }\n}\n\nexport function createRenderPage(distPath?: string) : RenderPage {\n // Per-handler caches for the immutable production SSR assets: the dist\n // template, manifest and server bundle do not change after boot, so read\n // them once instead of per request (the auth/login routes are hot paths).\n // Held in this closure rather than at module scope so a second handler in\n // the process, with a substituted package, never renders another's\n // template. Only a resolved dist is kept, so a bundle built after boot is\n // picked up on the next request.\n let cachedDistPath: string | undefined;\n let cachedHtml: string | undefined;\n let cachedManifest: Record<string, any> | undefined;\n let cachedRender: RenderFunction | undefined;\n\n return async (event, config, ctx) => {\n const resolved = (cachedDistPath ??= resolveDistPath(distPath));\n if (!resolved) {\n throw new InternalError(\n 'The auth console bundle (@authup/client-auth-console) is not built or installed.',\n );\n }\n\n const html = (cachedHtml ??= await fs.promises.readFile(\n path.join(resolved, 'client', 'index.html'),\n 'utf-8',\n ));\n const manifest = (cachedManifest ??= JSON.parse(await fs.promises.readFile(\n path.join(resolved, 'client', '.vite', 'ssr-manifest.json'),\n 'utf-8',\n )));\n const render = (cachedRender ??= (await read(\n path.join(resolved, 'server', 'server.js'),\n )).render as RenderFunction);\n\n // The client app hydrates locale + color-mode from the payload, so it\n // adopts the same values the HTML shell is stamped with below.\n const preferences = readUIClientPreferences(event);\n\n // `ui_locales` is what the RP says the visitor is reading over there,\n // which is fresher than anything this origin knows about a visitor\n // arriving for the first time. It seeds the page only while they have\n // made no choice HERE: `vc-locale` holds `auto` until the switcher\n // writes a tag (@vuecs/locale never writes the resolved value back),\n // so an explicit choice keeps winning and the switcher cannot be\n // undone by the next authorize request carrying the hint. Seeding is\n // all it does -- the ref starts there, and nothing writes it back.\n if (!preferences.locale || preferences.locale === 'auto') {\n preferences.locale = readUILocalesHint(event) || preferences.locale;\n }\n\n // `ui_color_mode` the same way, and `system` is its `auto`: the\n // switcher is a binary toggle, so it only ever writes `light` or\n // `dark`, and anything else in the cookie means the visitor never\n // touched it here.\n if (!preferences.colorMode || preferences.colorMode === 'system') {\n preferences.colorMode = readUIColorModeHint(event) || preferences.colorMode;\n }\n\n // `baseURL` is server-core's public URL: the console derives its HTTP\n // client and its cookie path from it. `basePath` is where THIS service\n // is served, which is what the router and every inter-page href need.\n // Before the split the two were the same value; they are not any more,\n // so the console reads them separately.\n const basePath = getURLBasePath(config.url);\n\n const payload : HydrationPayload = {\n config: {\n baseURL: config.apiUrl,\n basePath,\n colorMode: preferences.colorMode,\n locale: preferences.locale,\n },\n data: ctx.data,\n };\n\n const [appHtml, preloadLinks] = await render({\n url: ctx.url,\n manifest,\n payload,\n });\n\n let body = replaceTemplateMarker(html, '<!--preload-links-->', preloadLinks);\n body = replaceTemplateMarker(body, '<!--app-html-->', appHtml);\n\n body = stampHtmlAttributes(body, preferences);\n\n // This service mounts the bundle's assets at its own /assets, so the\n // fixed vite base in every href is replaced by this service's public\n // path. The vite base was decided when the bundle was built and says\n // nothing about where the service is published.\n body = rebaseAssetURLs(body, VITE_BASE, `${basePath}/`);\n\n // The theme's own asset urls are built from the same base, and the\n // service mounts them under it, so a themed deployment needs no rule of\n // its own at the reverse proxy beyond the one that reaches this service.\n body = await applyTheme(body, ctx.theme, basePath);\n\n applyUIPageHeaders(event);\n\n return body;\n };\n}\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport type { IThemeProvider } from '@authup/server-console-kit';\nimport {\n THEME_ASSET_MOUNT_PATH,\n createThemeAssetsHandler,\n createThemeProvider,\n} from '@authup/server-console-kit';\nimport { createHandler as createAssetsHandler } from '@routup/assets';\nimport { basic } from '@routup/basic';\nimport path from 'node:path';\nimport type { IApp } from 'routup';\nimport { App, defineCoreHandler } from 'routup';\nimport { ASSETS_PATH, HEALTH_PATH } from './constants';\nimport {\n buildWorkflowPageData,\n createAPIClient,\n createFeaturesReader,\n readAuthorizeInfo,\n readDeviceError,\n readDeviceFederatedLogin,\n readDeviceUserCode,\n} from './payload';\nimport { assertRenderContract, createRenderPage } from './render';\nimport { resolveDistPath } from './resolve';\nimport type { Config, RenderPage } from './types';\n\nconst WORKFLOW_PAGES : {\n url: string,\n realmAware?: boolean,\n tokenAware?: boolean\n}[] = [\n { url: '/register', realmAware: true },\n { url: '/activate', tokenAware: true },\n { url: '/password-forgot', realmAware: true },\n {\n url: '/password-reset',\n realmAware: true,\n tokenAware: true,\n },\n];\n\n/**\n * The service as a mountable routup handler, so the CLI can compose it onto\n * server-core's own listener for the single-container deployment while a\n * split deployment runs it behind its own server.\n *\n * The paths are prefix-free. authup assumes a prefix-stripping reverse\n * proxy, so a service published at `<origin>/console/auth` receives\n * `/authorize`, exactly as server-core received it before the split.\n */\nexport async function createHandler(\n config: Config,\n themeProvider?: IThemeProvider,\n render?: RenderPage,\n) : Promise<IApp> {\n const renderPage = render ?? createRenderPage(config.distPath);\n\n const app = new App();\n const client = createAPIClient(config);\n const readFeatures = createFeaturesReader(client);\n\n // The shell is stamped from the vc-locale / vc-color-mode cookies, and\n // without the plugin every cookie read answers undefined, which is a\n // silent flash of the wrong color mode on every full load. server-core\n // registers it globally, so a mounted handler inherits one; a standalone\n // service has nothing above it.\n app.use(basic({ cookie: true, query: true }));\n\n // A missing directory disables theming entirely: no provider is created\n // and the rendered pages are byte-identical to the un-themed ones, so the\n // default configuration pays nothing. An invalid manifest throws here and\n // fails the boot rather than surfacing per request.\n // Injected by the theme module when this runs inside the console\n // application; built here for a caller holding only a config.\n const theme = themeProvider ?? await createThemeProvider(config);\n\n app.use(defineCoreHandler({\n method: 'get',\n path: HEALTH_PATH,\n fn: () => ({ status: 'ok' }),\n }));\n\n if (theme) {\n app.use(THEME_ASSET_MOUNT_PATH, createThemeAssetsHandler(theme));\n }\n\n app.use(defineCoreHandler({\n method: 'get',\n path: '/authorize',\n fn: async (event) => renderPage(event, config, {\n url: '/authorize',\n data: await readAuthorizeInfo(client, event),\n theme,\n }),\n }));\n\n for (const page of WORKFLOW_PAGES) {\n app.use(defineCoreHandler({\n method: 'get',\n path: page.url,\n fn: async (event) => {\n const features = await readFeatures();\n\n return renderPage(event, config, {\n url: page.url,\n data: buildWorkflowPageData(event, features, page),\n theme,\n });\n },\n }));\n }\n\n // The logout page needs nothing resolved up front: it drives the\n // end-session call itself and renders from the answer, so the render\n // stays a pure shell and no request reaches server-core here.\n app.use(defineCoreHandler({\n method: 'get',\n path: '/logout',\n fn: async (event) => renderPage(event, config, {\n url: '/logout',\n data: {},\n theme,\n }),\n }));\n\n app.use(defineCoreHandler({\n method: 'get',\n path: '/device',\n fn: async (event) => renderPage(event, config, {\n url: '/device',\n data: {\n features: await readFeatures(),\n userCode: readDeviceUserCode(event),\n federatedLogin: readDeviceFederatedLogin(event),\n error: readDeviceError(event),\n },\n theme,\n }),\n }));\n\n // Only the assets directory is mounted, never dist/client itself: the\n // template and the ssr manifest are inputs of the render, not files to\n // serve. A missing bundle only disables the mount; the page routes\n // report the actionable error. A resolved one is checked against the\n // render contract here, unless a substituted render never reads it.\n const distPath = resolveDistPath(config.distPath);\n if (distPath) {\n if (!render) {\n await assertRenderContract(distPath);\n }\n\n // Every name carries a content hash, so a new build means new names.\n app.use(ASSETS_PATH, createAssetsHandler(\n path.posix.join(distPath, 'client', 'assets'),\n {\n fallthrough: false,\n scan: false,\n cacheMaxAge: 60 * 60 * 24 * 365,\n cacheImmutable: true,\n },\n ));\n }\n\n return app;\n}\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport { createApplication as createConsoleApplication } from '@authup/server-console-kit';\nimport type { CreateApplicationContext } from '@authup/server-console-kit';\nimport type { Application } from 'orkos';\nimport { readConfigFromEnv } from './config';\nimport { createHandler } from './handler';\nimport type { Config } from './types';\n\n/**\n * This console as a runnable service: its own module graph, its own\n * container, its own lifecycle, and nothing of server-core's. It is what\n * `authup start console` starts, what the `authup-auth-console` bin starts, and what\n * `authup start` composes onto its own listener, so no caller reaches past\n * the graph for a bare handler.\n *\n * `config` defaults to a FACTORY, not a value: resolving it reads the\n * document, and a console started alongside others must not do that at\n * construction time, before the caller has said where to look. Pass\n * `listen: false` to be mounted on someone else's listener.\n */\nexport function createApplication(\n context: Partial<Omit<CreateApplicationContext<Config>, 'createHandler'>> = {},\n) : Application {\n return createConsoleApplication<Config>({\n listen: context.listen,\n config: context.config ?? readConfigFromEnv,\n createHandler: (resolved, theme) => createHandler(resolved, theme),\n });\n}\n"],"mappings":"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"}