@authup/server-core 1.0.0-beta.61 → 1.0.0-beta.63
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.
- package/dist/adapters/database/domains/client-scope/entity.d.ts.map +1 -1
- package/dist/adapters/database/domains/client-scope/entity.mjs +16 -8
- package/dist/adapters/database/domains/client-scope/entity.mjs.map +1 -1
- package/dist/adapters/database/domains/identity-provider-account/entity.d.ts.map +1 -1
- package/dist/adapters/database/domains/identity-provider-account/entity.mjs +1 -1
- package/dist/adapters/database/domains/identity-provider-account/entity.mjs.map +1 -1
- package/dist/adapters/database/domains/identity-provider-attribute/subscriber.d.ts +8 -1
- package/dist/adapters/database/domains/identity-provider-attribute/subscriber.d.ts.map +1 -1
- package/dist/adapters/database/domains/identity-provider-attribute/subscriber.mjs +14 -0
- package/dist/adapters/database/domains/identity-provider-attribute/subscriber.mjs.map +1 -1
- package/dist/adapters/database/migrations/mysql/1786631686318-SortIndexesAndAccountUniqueness.d.ts +50 -0
- package/dist/adapters/database/migrations/mysql/1786631686318-SortIndexesAndAccountUniqueness.d.ts.map +1 -0
- package/dist/adapters/database/migrations/mysql/1786631686318-SortIndexesAndAccountUniqueness.mjs +86 -0
- package/dist/adapters/database/migrations/mysql/1786631686318-SortIndexesAndAccountUniqueness.mjs.map +1 -0
- package/dist/adapters/database/migrations/postgres/1786631686318-SortIndexesAndAccountUniqueness.d.ts +42 -0
- package/dist/adapters/database/migrations/postgres/1786631686318-SortIndexesAndAccountUniqueness.d.ts.map +1 -0
- package/dist/adapters/database/migrations/postgres/1786631686318-SortIndexesAndAccountUniqueness.mjs +80 -0
- package/dist/adapters/database/migrations/postgres/1786631686318-SortIndexesAndAccountUniqueness.mjs.map +1 -0
- package/dist/adapters/http/adapters/oauth2/grant-types/refresh_token.d.ts.map +1 -1
- package/dist/adapters/http/adapters/oauth2/grant-types/refresh_token.mjs +8 -2
- package/dist/adapters/http/adapters/oauth2/grant-types/refresh_token.mjs.map +1 -1
- package/dist/adapters/http/adapters/oauth2/grant-types/utils/credentials.mjs +1 -1
- package/dist/adapters/http/adapters/oauth2/grant-types/utils/guess.mjs +1 -1
- package/dist/adapters/http/controllers/entities/client/module.mjs +2 -2
- package/dist/adapters/http/controllers/entities/identity-provider/module.d.ts +24 -4
- package/dist/adapters/http/controllers/entities/identity-provider/module.d.ts.map +1 -1
- package/dist/adapters/http/controllers/entities/identity-provider/module.mjs +117 -106
- package/dist/adapters/http/controllers/entities/identity-provider/module.mjs.map +1 -1
- package/dist/adapters/http/controllers/entities/identity-provider/types.d.ts +6 -4
- package/dist/adapters/http/controllers/entities/identity-provider/types.d.ts.map +1 -1
- package/dist/adapters/http/controllers/entities/realm/module.mjs +2 -2
- package/dist/adapters/http/controllers/entities/session/module.mjs +2 -2
- package/dist/adapters/http/controllers/entities/user/module.mjs +2 -2
- package/dist/adapters/http/controllers/entities/user-authenticator/module.mjs +2 -2
- package/dist/adapters/http/controllers/workflows/authenticator-challenge/module.d.ts +2 -0
- package/dist/adapters/http/controllers/workflows/authenticator-challenge/module.d.ts.map +1 -1
- package/dist/adapters/http/controllers/workflows/authenticator-challenge/module.mjs +21 -3
- package/dist/adapters/http/controllers/workflows/authenticator-challenge/module.mjs.map +1 -1
- package/dist/adapters/http/controllers/workflows/authorize/module.d.ts.map +1 -1
- package/dist/adapters/http/controllers/workflows/authorize/module.mjs +14 -5
- package/dist/adapters/http/controllers/workflows/authorize/module.mjs.map +1 -1
- package/dist/adapters/http/controllers/workflows/token/module.d.ts.map +1 -1
- package/dist/adapters/http/controllers/workflows/token/module.mjs +22 -9
- package/dist/adapters/http/controllers/workflows/token/module.mjs.map +1 -1
- package/dist/adapters/http/controllers/workflows/token/utils/extract.mjs +2 -1
- package/dist/adapters/http/controllers/workflows/token/utils/extract.mjs.map +1 -1
- package/dist/adapters/http/middleware/built-in/assets.mjs +1 -1
- package/dist/adapters/http/middleware/built-in/cors.d.ts.map +1 -1
- package/dist/adapters/http/middleware/built-in/cors.mjs.map +1 -1
- package/dist/adapters/http/middleware/built-in/error.d.ts.map +1 -1
- package/dist/adapters/http/middleware/built-in/error.mjs +10 -1
- package/dist/adapters/http/middleware/built-in/error.mjs.map +1 -1
- package/dist/adapters/http/request/helpers/param-id.d.ts.map +1 -1
- package/dist/adapters/http/request/helpers/param-id.mjs +2 -1
- package/dist/adapters/http/request/helpers/param-id.mjs.map +1 -1
- package/dist/adapters/http/ui/account-console/module.mjs +1 -1
- package/dist/adapters/http/ui/auth-console/http-client.mjs +1 -1
- package/dist/adapters/http/ui/auth-console/module.mjs +1 -1
- package/dist/adapters/http/ui/console-packages/module.d.ts +1 -1
- package/dist/adapters/http/ui/console-packages/module.mjs +2 -2
- package/dist/adapters/http/ui/console-packages/module.mjs.map +1 -1
- package/dist/adapters/http/ui/shared/html.d.ts.map +1 -1
- package/dist/adapters/http/ui/shared/html.mjs +1 -0
- package/dist/adapters/http/ui/shared/html.mjs.map +1 -1
- package/dist/app/modules/config/normalize.d.ts.map +1 -1
- package/dist/app/modules/config/normalize.mjs +2 -2
- package/dist/app/modules/config/normalize.mjs.map +1 -1
- package/dist/app/modules/config/read/env.mjs +1 -1
- package/dist/app/modules/config/types.d.ts +8 -0
- package/dist/app/modules/config/types.d.ts.map +1 -1
- package/dist/app/modules/database/repositories/event/repository.d.ts +2 -2
- package/dist/app/modules/database/repositories/event/repository.d.ts.map +1 -1
- package/dist/app/modules/database/repositories/event/repository.mjs +23 -6
- package/dist/app/modules/database/repositories/event/repository.mjs.map +1 -1
- package/dist/app/modules/database/repositories/identity-provider/repository.d.ts +3 -1
- package/dist/app/modules/database/repositories/identity-provider/repository.d.ts.map +1 -1
- package/dist/app/modules/database/repositories/identity-provider/repository.mjs +2 -2
- package/dist/app/modules/database/repositories/identity-provider/repository.mjs.map +1 -1
- package/dist/app/modules/database/repositories/identity-provider-account/repository.d.ts.map +1 -1
- package/dist/app/modules/database/repositories/identity-provider-account/repository.mjs +9 -1
- package/dist/app/modules/database/repositories/identity-provider-account/repository.mjs.map +1 -1
- package/dist/app/modules/http/modules/controller.d.ts.map +1 -1
- package/dist/app/modules/http/modules/controller.mjs +20 -6
- package/dist/app/modules/http/modules/controller.mjs.map +1 -1
- package/dist/app/modules/identity/module.d.ts.map +1 -1
- package/dist/app/modules/identity/module.mjs +6 -3
- package/dist/app/modules/identity/module.mjs.map +1 -1
- package/dist/app/modules/identity/repositories/user.d.ts +1 -0
- package/dist/app/modules/identity/repositories/user.d.ts.map +1 -1
- package/dist/app/modules/identity/repositories/user.mjs +3 -0
- package/dist/app/modules/identity/repositories/user.mjs.map +1 -1
- package/dist/app/modules/oauth2/constants.d.ts +2 -1
- package/dist/app/modules/oauth2/constants.d.ts.map +1 -1
- package/dist/app/modules/oauth2/constants.mjs +1 -0
- package/dist/app/modules/oauth2/constants.mjs.map +1 -1
- package/dist/app/modules/oauth2/module.d.ts.map +1 -1
- package/dist/app/modules/oauth2/module.mjs +5 -0
- package/dist/app/modules/oauth2/module.mjs.map +1 -1
- package/dist/app/modules/oauth2/repositories/authorize/state/repository.d.ts +1 -2
- package/dist/app/modules/oauth2/repositories/authorize/state/repository.d.ts.map +1 -1
- package/dist/app/modules/oauth2/repositories/authorize/state/repository.mjs +7 -16
- package/dist/app/modules/oauth2/repositories/authorize/state/repository.mjs.map +1 -1
- package/dist/app/modules/oauth2/repositories/constants.d.ts +1 -0
- package/dist/app/modules/oauth2/repositories/constants.d.ts.map +1 -1
- package/dist/app/modules/oauth2/repositories/constants.mjs +1 -0
- package/dist/app/modules/oauth2/repositories/constants.mjs.map +1 -1
- package/dist/app/modules/oauth2/repositories/federated-login/index.d.ts +2 -0
- package/dist/app/modules/oauth2/repositories/federated-login/index.d.ts.map +1 -0
- package/dist/app/modules/oauth2/repositories/federated-login/index.mjs +5 -0
- package/dist/app/modules/oauth2/repositories/federated-login/store.d.ts +9 -0
- package/dist/app/modules/oauth2/repositories/federated-login/store.d.ts.map +1 -0
- package/dist/app/modules/oauth2/repositories/federated-login/store.mjs +33 -0
- package/dist/app/modules/oauth2/repositories/federated-login/store.mjs.map +1 -0
- package/dist/app/modules/oauth2/repositories/index.d.ts +1 -0
- package/dist/app/modules/oauth2/repositories/index.d.ts.map +1 -1
- package/dist/app/modules/oauth2/repositories/index.mjs +3 -1
- package/dist/app/modules/runtime/module.mjs +2 -2
- package/dist/cli/commands/migration.mjs +1 -1
- package/dist/core/entities/client-scope/schema.d.ts.map +1 -1
- package/dist/core/entities/client-scope/schema.mjs +16 -1
- package/dist/core/entities/client-scope/schema.mjs.map +1 -1
- package/dist/core/entities/event/constants.d.ts +9 -0
- package/dist/core/entities/event/constants.d.ts.map +1 -1
- package/dist/core/entities/event/constants.mjs +9 -1
- package/dist/core/entities/event/constants.mjs.map +1 -1
- package/dist/core/entities/event/entity-event-handler.d.ts.map +1 -1
- package/dist/core/entities/event/entity-event-handler.mjs +14 -1
- package/dist/core/entities/event/entity-event-handler.mjs.map +1 -1
- package/dist/core/entities/event/index.mjs +2 -2
- package/dist/core/entities/event/types.d.ts +11 -2
- package/dist/core/entities/event/types.d.ts.map +1 -1
- package/dist/core/entities/identity-provider/types.d.ts +9 -1
- package/dist/core/entities/identity-provider/types.d.ts.map +1 -1
- package/dist/core/entities/identity-provider-account/types.d.ts +6 -0
- package/dist/core/entities/identity-provider-account/types.d.ts.map +1 -1
- package/dist/core/entities/identity-provider-account/types.mjs.map +1 -1
- package/dist/core/entities/index.mjs +2 -2
- package/dist/core/identity/entities/user/types.d.ts +1 -0
- package/dist/core/identity/entities/user/types.d.ts.map +1 -1
- package/dist/core/identity/index.mjs +12 -11
- package/dist/core/identity/provider/account/module.d.ts +3 -0
- package/dist/core/identity/provider/account/module.d.ts.map +1 -1
- package/dist/core/identity/provider/account/module.mjs +54 -27
- package/dist/core/identity/provider/account/module.mjs.map +1 -1
- package/dist/core/identity/provider/account/types.d.ts +7 -0
- package/dist/core/identity/provider/account/types.d.ts.map +1 -1
- package/dist/core/identity/provider/authentication/index.mjs +3 -2
- package/dist/core/identity/provider/authentication/protocols/index.mjs +2 -1
- package/dist/core/identity/provider/authentication/protocols/oauth2/assurance.d.ts +46 -0
- package/dist/core/identity/provider/authentication/protocols/oauth2/assurance.d.ts.map +1 -0
- package/dist/core/identity/provider/authentication/protocols/oauth2/assurance.mjs +89 -0
- package/dist/core/identity/provider/authentication/protocols/oauth2/assurance.mjs.map +1 -0
- package/dist/core/identity/provider/authentication/protocols/oauth2/index.d.ts +1 -0
- package/dist/core/identity/provider/authentication/protocols/oauth2/index.d.ts.map +1 -1
- package/dist/core/identity/provider/authentication/protocols/oauth2/index.mjs +2 -1
- package/dist/core/identity/provider/authentication/protocols/oauth2/module.d.ts.map +1 -1
- package/dist/core/identity/provider/authentication/protocols/oauth2/module.mjs +9 -2
- package/dist/core/identity/provider/authentication/protocols/oauth2/module.mjs.map +1 -1
- package/dist/core/identity/provider/index.mjs +12 -11
- package/dist/core/index.mjs +16 -12
- package/dist/core/oauth2/authorization/code-request/verifier/module.d.ts.map +1 -1
- package/dist/core/oauth2/authorization/code-request/verifier/module.mjs +7 -1
- package/dist/core/oauth2/authorization/code-request/verifier/module.mjs.map +1 -1
- package/dist/core/oauth2/authorization/helpers.d.ts.map +1 -1
- package/dist/core/oauth2/authorization/helpers.mjs +6 -1
- package/dist/core/oauth2/authorization/helpers.mjs.map +1 -1
- package/dist/core/oauth2/authorization/module.d.ts.map +1 -1
- package/dist/core/oauth2/authorization/module.mjs +5 -3
- package/dist/core/oauth2/authorization/module.mjs.map +1 -1
- package/dist/core/oauth2/authorization/state/module.mjs +1 -2
- package/dist/core/oauth2/authorization/state/module.mjs.map +1 -1
- package/dist/core/oauth2/authorization/state/types.d.ts +13 -2
- package/dist/core/oauth2/authorization/state/types.d.ts.map +1 -1
- package/dist/core/oauth2/end-session/service.d.ts.map +1 -1
- package/dist/core/oauth2/end-session/service.mjs +1 -0
- package/dist/core/oauth2/end-session/service.mjs.map +1 -1
- package/dist/core/oauth2/federated-login/constants.d.ts +22 -0
- package/dist/core/oauth2/federated-login/constants.d.ts.map +1 -0
- package/dist/core/oauth2/federated-login/constants.mjs +27 -0
- package/dist/core/oauth2/federated-login/constants.mjs.map +1 -0
- package/dist/core/oauth2/federated-login/index.d.ts +4 -0
- package/dist/core/oauth2/federated-login/index.d.ts.map +1 -0
- package/dist/core/oauth2/federated-login/index.mjs +7 -0
- package/dist/core/oauth2/federated-login/module.d.ts +29 -0
- package/dist/core/oauth2/federated-login/module.d.ts.map +1 -0
- package/dist/core/oauth2/federated-login/module.mjs +193 -0
- package/dist/core/oauth2/federated-login/module.mjs.map +1 -0
- package/dist/core/oauth2/federated-login/types.d.ts +196 -0
- package/dist/core/oauth2/federated-login/types.d.ts.map +1 -0
- package/dist/core/oauth2/federated-login/types.mjs +36 -0
- package/dist/core/oauth2/federated-login/types.mjs.map +1 -0
- package/dist/core/oauth2/grant-types/authorize.mjs +1 -1
- package/dist/core/oauth2/grant-types/client-credentials.mjs +1 -1
- package/dist/core/oauth2/grant-types/identity.mjs +1 -1
- package/dist/core/oauth2/grant-types/password.mjs +1 -1
- package/dist/core/oauth2/grant-types/refresh-token.mjs +1 -1
- package/dist/core/oauth2/index.d.ts +1 -0
- package/dist/core/oauth2/index.d.ts.map +1 -1
- package/dist/core/oauth2/index.mjs +5 -1
- package/dist/core/oauth2/token/verifier/module.d.ts.map +1 -1
- package/dist/core/oauth2/token/verifier/module.mjs +3 -3
- package/dist/core/oauth2/token/verifier/module.mjs.map +1 -1
- package/dist/core/provisioning/entities/policy/validator.d.ts.map +1 -1
- package/dist/core/provisioning/entities/policy/validator.mjs +2 -1
- package/dist/core/provisioning/entities/policy/validator.mjs.map +1 -1
- package/dist/core/provisioning/entities/realm/wildcard-validator.d.ts.map +1 -1
- package/dist/core/provisioning/entities/realm/wildcard-validator.mjs +2 -1
- package/dist/core/provisioning/entities/realm/wildcard-validator.mjs.map +1 -1
- package/dist/core/security/policy/evaluator.mjs.map +1 -1
- package/dist/node_modules/@ebec/core/dist/index.mjs +130 -0
- package/dist/node_modules/@ebec/core/dist/index.mjs.map +1 -0
- package/dist/swagger.json +119 -43
- package/dist/utils/error.d.ts.map +1 -1
- package/dist/utils/error.mjs +5 -6
- package/dist/utils/error.mjs.map +1 -1
- package/package.json +29 -29
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module.mjs","names":["DContext","DController","DGet","DPost","URL","useRequestQuery","OAuth2AccessDeniedError","OAuth2ErrorCode","isOAuth2AccessDeniedError","ForceUserLoggedInMiddleware","HTTPOAuth2Authorizer","renderAuthConsolePage","readFromLocations","OAuth2AuthorizationCodeRequestValidator","sanitizeError","AuthorizeController","options","codeRequestVerifier","codeRequestValidator","authorizer","ctx","codeIssuer","sessionManager","eventService","metrics","promptLoginMaxAge","mfaFreshnessMaxAge","mfaChallengeProvider","accessPolicyEvaluator","consentService","logger","confirm","event","result","authorizeWithRequest","url","redirectUri","state","searchParams","set","authorizationCode","href","e","ACCESS_DENIED","serve","codeRequest","client","scopes","realm","redirectUriVerified","error","merged","data","run","verify","id","name","displayName","builtIn","createdAt","normalized","message","query","forClient","requestURL","request","payload","config","baseURL","features","requestPath","pathname","search"],"sources":["../../../../../../src/adapters/http/controllers/workflows/authorize/module.ts"],"sourcesContent":["/*\n * Copyright (c) 2024.\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 DContext,\n DController,\n DGet,\n DPost,\n} from '@routup/decorators';\nimport { URL } from 'node:url';\n\nimport type { IAppEvent } from 'routup';\nimport { useRequestQuery } from '@routup/basic/query';\nimport type {\n Client,\n OAuth2AuthorizationCodeRequest,\n Realm,\n Scope,\n} from '@authup/core-kit';\nimport { OAuth2AccessDeniedError, OAuth2ErrorCode, isOAuth2AccessDeniedError } from '@authup/specs';\nimport { ForceUserLoggedInMiddleware } from '../../../middleware/index.ts';\nimport { HTTPOAuth2Authorizer } from '../../../adapters/index.ts';\nimport { renderAuthConsolePage } from '../../../ui/index.ts';\nimport { readFromLocations } from '../../../request/index.ts';\nimport type { IOAuth2AuthorizationCodeRequestVerifier } from '../../../../../core/index.ts';\nimport { OAuth2AuthorizationCodeRequestValidator } from '../../../../../core/index.ts';\nimport type { AuthorizeControllerContext, AuthorizeControllerOptions } from './types.ts';\nimport { sanitizeError } from '../../../../../utils/index.ts';\n\ntype RealmSummary = Pick<Realm, 'id' | 'name' | 'displayName'>;\ntype ClientSummary = Pick<Client, 'id' | 'name' | 'displayName' | 'builtIn' | 'createdAt'>;\n\n@DController('/authorize')\nexport class AuthorizeController {\n // todo: maybe /realms/<realm>/protocol/openid-connect/authorize\n // todo: maybe /realms/<realm>/[...]/authorize\n\n protected options: AuthorizeControllerOptions;\n\n protected codeRequestVerifier : IOAuth2AuthorizationCodeRequestVerifier;\n\n protected codeRequestValidator : OAuth2AuthorizationCodeRequestValidator;\n\n protected authorizer: HTTPOAuth2Authorizer;\n\n // ---------------------------------------------------------\n\n constructor(ctx: AuthorizeControllerContext) {\n this.options = ctx.options;\n\n this.codeRequestVerifier = ctx.codeRequestVerifier;\n\n this.codeRequestValidator = new OAuth2AuthorizationCodeRequestValidator();\n\n this.authorizer = new HTTPOAuth2Authorizer({\n codeRequestVerifier: this.codeRequestVerifier,\n codeIssuer: ctx.codeIssuer,\n sessionManager: ctx.sessionManager,\n eventService: ctx.eventService,\n metrics: ctx.metrics,\n promptLoginMaxAge: ctx.options.promptLoginMaxAge,\n mfaFreshnessMaxAge: ctx.options.mfaFreshnessMaxAge,\n mfaChallengeProvider: ctx.mfaChallengeProvider,\n accessPolicyEvaluator: ctx.accessPolicyEvaluator,\n consentService: ctx.consentService,\n logger: ctx.logger,\n });\n }\n\n // ---------------------------------------------------------\n\n @DPost('', [ForceUserLoggedInMiddleware])\n async confirm(@DContext() event: IAppEvent): Promise<{ url: string }> {\n try {\n const result = await this.authorizer.authorizeWithRequest(event);\n\n const url = new URL(result.redirectUri);\n if (result.state) {\n url.searchParams.set('state', result.state);\n }\n\n if (result.authorizationCode) {\n url.searchParams.set('code', result.authorizationCode);\n }\n\n return { url: url.href };\n } catch (e) {\n // Access-policy denial (plan 052): with a pattern-verified\n // redirect_uri the denial is answered as an error redirect\n // (RFC 6749 §4.1.2.1) — the kit navigates the returned url like\n // any success. Unverified → rethrow → 400 body → interactive\n // denial card.\n if (isOAuth2AccessDeniedError(e) && e.redirectUri) {\n const url = new URL(e.redirectUri);\n url.searchParams.set('error', OAuth2ErrorCode.ACCESS_DENIED);\n if (e.state) {\n url.searchParams.set('state', e.state);\n }\n\n return { url: url.href };\n }\n\n throw e;\n }\n }\n\n @DGet('', [])\n async serve(@DContext() event: IAppEvent): Promise<string> {\n let codeRequest : OAuth2AuthorizationCodeRequest | undefined;\n\n let client : ClientSummary | undefined;\n let scopes : Scope[] | undefined;\n\n // Target realm summary (the client's realm) — the UI names it in the\n // realm-mismatch notice. codeRequest.realm_id is only the id.\n let realm : RealmSummary | undefined;\n\n // Whether the request redirect_uri matched a registered client pattern —\n // the UI must not offer an automatic \"return to application\" redirect to\n // an unverified (pattern-less-client) redirect_uri.\n let redirectUriVerified = false;\n\n let error : Error | undefined;\n\n try {\n const merged = await readFromLocations(event, ['body', 'query']);\n const data = await this.codeRequestValidator.run(merged);\n\n const result = await this.codeRequestVerifier.verify(data);\n\n // Trim the client to a deliberate DTO before it reaches the\n // anonymous SSR hydration payload — never disclose redirect_uri\n // patterns (the trusted-origin set), grant_types, internal\n // base/root URLs, auth methods, or the secret storage flags to an\n // unauthenticated visitor. (ClientEntity.secret is additionally\n // select:false, so the secret is never loaded — but the DTO must not\n // rely on that alone: a future entity/query change could unset it.)\n client = {\n id: result.client.id,\n name: result.client.name,\n displayName: result.client.displayName,\n builtIn: result.client.builtIn,\n createdAt: result.client.createdAt,\n };\n scopes = result.scopes;\n redirectUriVerified = result.redirectUriVerified;\n\n if (result.client.realm) {\n realm = {\n id: result.client.realm.id,\n name: result.client.realm.name,\n displayName: result.client.realm.displayName,\n };\n }\n\n codeRequest = result.data;\n } catch (e) {\n const normalized = sanitizeError(e);\n error = {\n ...normalized,\n message: normalized.message,\n };\n }\n\n // A recognized `error` query param (the federated-IdP callback\n // redirects back here with error=access_denied on a policy denial)\n // is mapped onto the hydration payload's error — neutral message,\n // never attacker-controlled text.\n if (!error) {\n const query = useRequestQuery(event);\n if (query.error === OAuth2ErrorCode.ACCESS_DENIED) {\n const normalized = sanitizeError(OAuth2AccessDeniedError.forClient());\n error = {\n ...normalized,\n message: normalized.message,\n };\n }\n }\n\n // Path + query of the original request — the SSR page uses it as\n // the same-origin `redirect` parameter on register / password links\n // so those pages can lead back into this authorize request.\n const requestURL = new URL(event.request.url);\n\n return renderAuthConsolePage(event, {\n url: '/authorize',\n payload: {\n config: { baseURL: this.options.baseURL },\n data: {\n codeRequest,\n error,\n client,\n scopes,\n realm,\n redirectUriVerified,\n features: this.options.features,\n requestPath: `${requestURL.pathname}${requestURL.search}`,\n },\n },\n });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"module.mjs","names":["DContext","DController","DGet","DPost","URL","isUUID","useRequestQuery","OAuth2AccessDeniedError","OAuth2ErrorCode","OAuth2LoginRequiredError","isOAuth2AccessDeniedError","ForceUserLoggedInMiddleware","HTTPOAuth2Authorizer","renderAuthConsolePage","readFromLocations","OAuth2AuthorizationCodeRequestValidator","sanitizeError","AuthorizeController","options","codeRequestVerifier","codeRequestValidator","authorizer","ctx","codeIssuer","sessionManager","eventService","metrics","promptLoginMaxAge","mfaFreshnessMaxAge","mfaChallengeProvider","accessPolicyEvaluator","consentService","logger","confirm","event","result","authorizeWithRequest","url","redirectUri","state","searchParams","set","authorizationCode","href","e","ACCESS_DENIED","serve","codeRequest","client","scopes","realm","redirectUriVerified","error","merged","data","run","verify","id","name","displayName","builtIn","createdAt","normalized","message","query","mapped","forClient","LOGIN_REQUIRED","providerUnavailable","federatedLogin","provider","providerId","undefined","requestURL","request","delete","payload","config","baseURL","features","requestPath","pathname","search"],"sources":["../../../../../../src/adapters/http/controllers/workflows/authorize/module.ts"],"sourcesContent":["/*\n * Copyright (c) 2024.\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 DContext,\n DController,\n DGet,\n DPost,\n} from '@routup/decorators';\nimport { URL } from 'node:url';\n\nimport type { IAppEvent } from 'routup';\nimport { isUUID } from '@authup/kit';\nimport { useRequestQuery } from '@routup/basic/query';\nimport type {\n Client,\n OAuth2AuthorizationCodeRequest,\n Realm,\n Scope,\n} from '@authup/core-kit';\nimport {\n OAuth2AccessDeniedError,\n OAuth2ErrorCode,\n OAuth2LoginRequiredError,\n isOAuth2AccessDeniedError,\n} from '@authup/specs';\nimport { ForceUserLoggedInMiddleware } from '../../../middleware/index.ts';\nimport { HTTPOAuth2Authorizer } from '../../../adapters/index.ts';\nimport { renderAuthConsolePage } from '../../../ui/index.ts';\nimport { readFromLocations } from '../../../request/index.ts';\nimport type { IOAuth2AuthorizationCodeRequestVerifier } from '../../../../../core/index.ts';\nimport { OAuth2AuthorizationCodeRequestValidator } from '../../../../../core/index.ts';\nimport type { AuthorizeControllerContext, AuthorizeControllerOptions } from './types.ts';\nimport { sanitizeError } from '../../../../../utils/index.ts';\n\ntype RealmSummary = Pick<Realm, 'id' | 'name' | 'displayName'>;\ntype ClientSummary = Pick<Client, 'id' | 'name' | 'displayName' | 'builtIn' | 'createdAt'>;\n\n@DController('/authorize')\nexport class AuthorizeController {\n // todo: maybe /realms/<realm>/protocol/openid-connect/authorize\n // todo: maybe /realms/<realm>/[...]/authorize\n\n protected options: AuthorizeControllerOptions;\n\n protected codeRequestVerifier : IOAuth2AuthorizationCodeRequestVerifier;\n\n protected codeRequestValidator : OAuth2AuthorizationCodeRequestValidator;\n\n protected authorizer: HTTPOAuth2Authorizer;\n\n // ---------------------------------------------------------\n\n constructor(ctx: AuthorizeControllerContext) {\n this.options = ctx.options;\n\n this.codeRequestVerifier = ctx.codeRequestVerifier;\n\n this.codeRequestValidator = new OAuth2AuthorizationCodeRequestValidator();\n\n this.authorizer = new HTTPOAuth2Authorizer({\n codeRequestVerifier: this.codeRequestVerifier,\n codeIssuer: ctx.codeIssuer,\n sessionManager: ctx.sessionManager,\n eventService: ctx.eventService,\n metrics: ctx.metrics,\n promptLoginMaxAge: ctx.options.promptLoginMaxAge,\n mfaFreshnessMaxAge: ctx.options.mfaFreshnessMaxAge,\n mfaChallengeProvider: ctx.mfaChallengeProvider,\n accessPolicyEvaluator: ctx.accessPolicyEvaluator,\n consentService: ctx.consentService,\n logger: ctx.logger,\n });\n }\n\n // ---------------------------------------------------------\n\n @DPost('', [ForceUserLoggedInMiddleware])\n async confirm(@DContext() event: IAppEvent): Promise<{ url: string }> {\n try {\n const result = await this.authorizer.authorizeWithRequest(event);\n\n const url = new URL(result.redirectUri);\n if (result.state) {\n url.searchParams.set('state', result.state);\n }\n\n if (result.authorizationCode) {\n url.searchParams.set('code', result.authorizationCode);\n }\n\n return { url: url.href };\n } catch (e) {\n // Access-policy denial (plan 052): with a pattern-verified\n // redirect_uri the denial is answered as an error redirect\n // (RFC 6749 §4.1.2.1) — the kit navigates the returned url like\n // any success. Unverified → rethrow → 400 body → interactive\n // denial card.\n if (isOAuth2AccessDeniedError(e) && e.redirectUri) {\n const url = new URL(e.redirectUri);\n url.searchParams.set('error', OAuth2ErrorCode.ACCESS_DENIED);\n if (e.state) {\n url.searchParams.set('state', e.state);\n }\n\n return { url: url.href };\n }\n\n throw e;\n }\n }\n\n @DGet('', [])\n async serve(@DContext() event: IAppEvent): Promise<string> {\n let codeRequest : OAuth2AuthorizationCodeRequest | undefined;\n\n let client : ClientSummary | undefined;\n let scopes : Scope[] | undefined;\n\n // Target realm summary (the client's realm) — the UI names it in the\n // realm-mismatch notice. codeRequest.realm_id is only the id.\n let realm : RealmSummary | undefined;\n\n // Whether the request redirect_uri matched a registered client pattern —\n // the UI must not offer an automatic \"return to application\" redirect to\n // an unverified (pattern-less-client) redirect_uri.\n let redirectUriVerified = false;\n\n let error : Error | undefined;\n\n try {\n const merged = await readFromLocations(event, ['body', 'query']);\n const data = await this.codeRequestValidator.run(merged);\n\n const result = await this.codeRequestVerifier.verify(data);\n\n // Trim the client to a deliberate DTO before it reaches the\n // anonymous SSR hydration payload — never disclose redirect_uri\n // patterns (the trusted-origin set), grant_types, internal\n // base/root URLs, auth methods, or the secret storage flags to an\n // unauthenticated visitor. (ClientEntity.secret is additionally\n // select:false, so the secret is never loaded — but the DTO must not\n // rely on that alone: a future entity/query change could unset it.)\n client = {\n id: result.client.id,\n name: result.client.name,\n displayName: result.client.displayName,\n builtIn: result.client.builtIn,\n createdAt: result.client.createdAt,\n };\n scopes = result.scopes;\n redirectUriVerified = result.redirectUriVerified;\n\n if (result.client.realm) {\n realm = {\n id: result.client.realm.id,\n name: result.client.realm.name,\n displayName: result.client.realm.displayName,\n };\n }\n\n codeRequest = result.data;\n } catch (e) {\n const normalized = sanitizeError(e);\n error = {\n ...normalized,\n message: normalized.message,\n };\n }\n\n // A recognized `error` query param (the federated-IdP callback\n // redirects back here with one when it refuses a completion) is\n // mapped onto the hydration payload's error. The set stays closed and\n // the text is server-side and neutral, never attacker-controlled;\n // any other value is ignored.\n if (!error) {\n const query = useRequestQuery(event);\n let mapped : Error | undefined;\n if (query.error === OAuth2ErrorCode.ACCESS_DENIED) {\n mapped = OAuth2AccessDeniedError.forClient();\n } else if (query.error === OAuth2ErrorCode.LOGIN_REQUIRED) {\n mapped = OAuth2LoginRequiredError.providerUnavailable();\n }\n\n if (mapped) {\n const normalized = sanitizeError(mapped);\n error = {\n ...normalized,\n message: normalized.message,\n };\n }\n }\n\n // Path + query of the original request — the SSR page uses it as\n // the same-origin `redirect` parameter on register / password links\n // so those pages can lead back into this authorize request.\n // A federated callback sends the browser back here to complete the\n // login it established (plan 094). Only the provider is named: the\n // pending login itself rides a cookie, so nothing here is a secret.\n // It is interpolated into the completion request path by the page, so\n // it is accepted as an id and nothing else, and dropped from\n // `requestPath` so the register / password links do not carry it.\n const query = useRequestQuery(event);\n const federatedLogin = typeof query.provider === 'string' && isUUID(query.provider) ?\n { providerId: query.provider } :\n undefined;\n\n const requestURL = new URL(event.request.url);\n requestURL.searchParams.delete('provider');\n\n return renderAuthConsolePage(event, {\n url: '/authorize',\n payload: {\n config: { baseURL: this.options.baseURL },\n data: {\n codeRequest,\n error,\n client,\n scopes,\n realm,\n redirectUriVerified,\n federatedLogin,\n features: this.options.features,\n requestPath: `${requestURL.pathname}${requestURL.search}`,\n },\n },\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../../../../../../src/adapters/http/controllers/workflows/token/module.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,wBAAwB,EAAE,gCAAgC,EAAyB,MAAM,eAAe,CAAC;AACvH,OAAO,
|
|
1
|
+
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../../../../../../src/adapters/http/controllers/workflows/token/module.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,wBAAwB,EAAE,gCAAgC,EAAyB,MAAM,eAAe,CAAC;AACvH,OAAO,EAGH,gBAAgB,EAEnB,MAAM,eAAe,CAAC;AAQvB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAGxC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACzD,OAAO,KAAK,EACR,gBAAgB,EAChB,2BAA2B,EAC3B,iBAAiB,EACjB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACvB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAUnE,qBAEa,eAAe;IACxB,SAAS,CAAC,kBAAkB,EAAE,kBAAkB,CAAC;IAEjD,SAAS,CAAC,iBAAiB,EAAE,kBAAkB,CAAC;IAEhD,SAAS,CAAC,aAAa,EAAE,oBAAoB,CAAC;IAE9C,SAAS,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAE5C,SAAS,CAAC,gBAAgB,EAAG,iBAAiB,CAAC;IAE/C,SAAS,CAAC,0BAA0B,EAAG,2BAA2B,CAAC;IAEnE,SAAS,CAAC,OAAO,CAAC,EAAG,gBAAgB,CAAC;IAEtC,SAAS,CAAC,WAAW,EAAG,MAAM,CAAC,GAAG,gBAAgB,EAAE,EAAE,gBAAgB,CAAC,CAAC;gBAI5D,GAAG,EAAE,sBAAsB;IAgEjC,aAAa,CAAa,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,gCAAgC,CAAC;IAKtF,cAAc,CACJ,KAAK,EAAE,SAAS,GAC7B,OAAO,CAAC,gCAAgC,CAAC;IAoHtC,WAAW,CACD,KAAK,EAAE,SAAS,GAC7B,OAAO,CAAC,IAAI,CAAC;IA6CV,WAAW,CAAa,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,wBAAwB,CAAC;CAiBrF"}
|
|
@@ -14,7 +14,7 @@ import "../../../../../core/oauth2/helpers/index.mjs";
|
|
|
14
14
|
import { extractTokenFromRequest } from "./utils/extract.mjs";
|
|
15
15
|
import "./utils/index.mjs";
|
|
16
16
|
import { buildPermissionKey } from "@authup/access";
|
|
17
|
-
import { OAuth2GrantTypeError, OAuth2RequestError, OAuth2TokenGrant } from "@authup/specs";
|
|
17
|
+
import { OAuth2GrantTypeError, OAuth2RequestError, OAuth2TokenGrant, isJWTError } from "@authup/specs";
|
|
18
18
|
import { DContext, DController, DGet, DPost, DTags } from "@routup/decorators";
|
|
19
19
|
//#region src/adapters/http/controllers/workflows/token/module.ts
|
|
20
20
|
function _ts_decorate(decorators, target, key, desc) {
|
|
@@ -105,20 +105,28 @@ var TokenController = class {
|
|
|
105
105
|
async postIntrospect(event) {
|
|
106
106
|
try {
|
|
107
107
|
const token = await extractTokenFromRequest(event);
|
|
108
|
-
const payload = await this.tokenVerifier.verify(token, {
|
|
108
|
+
const payload = await this.tokenVerifier.verify(token, {
|
|
109
|
+
ignoreExpiry: true,
|
|
110
|
+
skipActiveCheck: true
|
|
111
|
+
});
|
|
109
112
|
if (!payload.sub || !payload.sub_kind) throw OAuth2RequestError.identityInvalid();
|
|
113
|
+
let active;
|
|
114
|
+
if (payload.jti && typeof payload.exp === "number") active = !await this.tokenVerifier.isInactive(payload.jti) && payload.exp * 1e3 > Date.now();
|
|
115
|
+
else active = false;
|
|
116
|
+
const identity = await this.identityResolver.resolve(payload.sub_kind, payload.sub);
|
|
117
|
+
if (!identity) throw OAuth2RequestError.identityInvalid();
|
|
118
|
+
const claims = new OAuth2OpenIDClaimsBuilder().fromIdentity(identity);
|
|
119
|
+
if (!active) return {
|
|
120
|
+
active,
|
|
121
|
+
...payload,
|
|
122
|
+
...claims
|
|
123
|
+
};
|
|
110
124
|
const permissions = await this.identityPermissionProvider.getFor({
|
|
111
125
|
id: payload.sub,
|
|
112
126
|
type: payload.sub_kind,
|
|
113
127
|
clientId: payload.client_id,
|
|
114
128
|
realmId: payload.realm_id
|
|
115
129
|
});
|
|
116
|
-
const identity = await this.identityResolver.resolve(payload.sub_kind, payload.sub);
|
|
117
|
-
if (!identity) throw OAuth2RequestError.identityInvalid();
|
|
118
|
-
const claims = new OAuth2OpenIDClaimsBuilder().fromIdentity(identity);
|
|
119
|
-
let active;
|
|
120
|
-
if (payload.jti) active = !await this.tokenVerifier.isInactive(payload.jti);
|
|
121
|
-
else active = false;
|
|
122
130
|
return {
|
|
123
131
|
active,
|
|
124
132
|
permissions: Object.values(permissions.reduce((acc, binding) => {
|
|
@@ -134,6 +142,7 @@ var TokenController = class {
|
|
|
134
142
|
...claims
|
|
135
143
|
};
|
|
136
144
|
} catch (e) {
|
|
145
|
+
if (isJWTError(e)) return { active: false };
|
|
137
146
|
throw toOAuth2Error(e);
|
|
138
147
|
}
|
|
139
148
|
}
|
|
@@ -145,9 +154,13 @@ var TokenController = class {
|
|
|
145
154
|
skipActiveCheck: true
|
|
146
155
|
});
|
|
147
156
|
await this.tokenRevoker.revoke(payload);
|
|
148
|
-
event.response.status =
|
|
157
|
+
event.response.status = 200;
|
|
149
158
|
return null;
|
|
150
159
|
} catch (e) {
|
|
160
|
+
if (isJWTError(e)) {
|
|
161
|
+
event.response.status = 200;
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
151
164
|
throw toOAuth2Error(e);
|
|
152
165
|
}
|
|
153
166
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module.mjs","names":["OAuth2GrantTypeError","OAuth2RequestError","OAuth2TokenGrant","DContext","DController","DGet","DPost","DTags","buildPermissionKey","toOAuth2Error","OAuth2OpenIDClaimsBuilder","HTTPClientCredentialsGrant","HTTPOAuth2AuthorizeGrant","HTTPOAuth2RefreshTokenGrant","HTTPPasswordGrant","guessOauth2GrantTypeByRequest","extractTokenFromRequest","TokenController","refreshTokenIssuer","accessTokenIssuer","tokenVerifier","tokenRevoker","identityResolver","identityPermissionProvider","metrics","tokenGrants","ctx","AUTHORIZATION_CODE","codeVerifier","clientAuthenticator","oauth2ClientAuthenticator","openIdTokenIssuer","keyStore","sessionManager","realmRepository","accessPolicyEvaluator","certificateSource","CLIENT_CREDENTIALS","PASSWORD","authenticator","userAuthenticator","eventService","loginThrottleService","userAuthenticatorService","mfaLoginService","logger","REFRESH_TOKEN","tokenRepository","sessionTokenRepository","options","gracePeriod","tokenRefreshGracePeriod","getIntrospect","event","postIntrospect","token","payload","verify","skipActiveCheck","sub","sub_kind","identityInvalid","permissions","getFor","id","type","clientId","client_id","realmId","realm_id","identity","resolve","claimsBuilder","claims","fromIdentity","active","jti","isInactive","Object","values","reduce","acc","binding","key","permission","name","e","revokeToken","ignoreExpiry","revoke","response","status","createToken","grantType","unsupported","grant","runWithRequest","recordTokenGrant"],"sources":["../../../../../../src/adapters/http/controllers/workflows/token/module.ts"],"sourcesContent":["/*\n * Copyright (c) 2022.\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 { OAuth2TokenGrantResponse, OAuth2TokenIntrospectionResponse, OAuth2TokenPermission } from '@authup/specs';\nimport { OAuth2GrantTypeError, OAuth2RequestError, OAuth2TokenGrant } from '@authup/specs';\nimport {\n DContext,\n DController,\n DGet,\n DPost,\n DTags,\n} from '@routup/decorators';\nimport type { IAppEvent } from 'routup';\nimport { buildPermissionKey } from '@authup/access';\nimport { toOAuth2Error } from '../../../../../core/oauth2/helpers/index.ts';\nimport type { TokenControllerContext } from './types.ts';\nimport type {\n IAuthFlowMetrics,\n IIdentityPermissionProvider,\n IIdentityResolver,\n IOAuth2TokenIssuer,\n IOAuth2TokenRevoker,\n IOAuth2TokenVerifier,\n} from '../../../../../core/index.ts';\nimport { OAuth2OpenIDClaimsBuilder } from '../../../../../core/index.ts';\nimport type { IHTTPOAuth2Grant } from '../../../adapters/index.ts';\nimport {\n HTTPClientCredentialsGrant,\n HTTPOAuth2AuthorizeGrant,\n HTTPOAuth2RefreshTokenGrant,\n HTTPPasswordGrant,\n guessOauth2GrantTypeByRequest,\n} from '../../../adapters/index.ts';\nimport { extractTokenFromRequest } from './utils/index.ts';\n\n@DTags('auth')\n@DController('/token')\nexport class TokenController {\n protected refreshTokenIssuer: IOAuth2TokenIssuer;\n\n protected accessTokenIssuer: IOAuth2TokenIssuer;\n\n protected tokenVerifier: IOAuth2TokenVerifier;\n\n protected tokenRevoker: IOAuth2TokenRevoker;\n\n protected identityResolver : IIdentityResolver;\n\n protected identityPermissionProvider : IIdentityPermissionProvider;\n\n protected metrics? : IAuthFlowMetrics;\n\n protected tokenGrants : Record<`${OAuth2TokenGrant}`, IHTTPOAuth2Grant>;\n\n // -------------------------------------------\n\n constructor(ctx: TokenControllerContext) {\n this.refreshTokenIssuer = ctx.refreshTokenIssuer;\n this.accessTokenIssuer = ctx.accessTokenIssuer;\n this.tokenVerifier = ctx.tokenVerifier;\n this.tokenRevoker = ctx.tokenRevoker;\n this.identityResolver = ctx.identityResolver;\n this.identityPermissionProvider = ctx.identityPermissionProvider;\n this.metrics = ctx.metrics;\n\n this.tokenGrants = {\n [OAuth2TokenGrant.AUTHORIZATION_CODE]: new HTTPOAuth2AuthorizeGrant({\n codeVerifier: ctx.codeVerifier,\n clientAuthenticator: ctx.oauth2ClientAuthenticator,\n accessTokenIssuer: ctx.accessTokenIssuer,\n refreshTokenIssuer: ctx.refreshTokenIssuer,\n openIdTokenIssuer: ctx.openIdTokenIssuer,\n keyStore: ctx.keyStore,\n sessionManager: ctx.sessionManager,\n realmRepository: ctx.realmRepository,\n accessPolicyEvaluator: ctx.accessPolicyEvaluator,\n certificateSource: ctx.certificateSource,\n }),\n [OAuth2TokenGrant.CLIENT_CREDENTIALS]: new HTTPClientCredentialsGrant({\n accessTokenIssuer: ctx.accessTokenIssuer,\n clientAuthenticator: ctx.oauth2ClientAuthenticator,\n sessionManager: ctx.sessionManager,\n certificateSource: ctx.certificateSource,\n }),\n [OAuth2TokenGrant.PASSWORD]: new HTTPPasswordGrant({\n accessTokenIssuer: ctx.accessTokenIssuer,\n refreshTokenIssuer: ctx.refreshTokenIssuer,\n authenticator: ctx.userAuthenticator,\n clientAuthenticator: ctx.oauth2ClientAuthenticator,\n sessionManager: ctx.sessionManager,\n realmRepository: ctx.realmRepository,\n eventService: ctx.eventService,\n metrics: ctx.metrics,\n loginThrottleService: ctx.loginThrottleService,\n userAuthenticatorService: ctx.userAuthenticatorService,\n mfaLoginService: ctx.mfaLoginService,\n logger: ctx.logger,\n certificateSource: ctx.certificateSource,\n }),\n [OAuth2TokenGrant.REFRESH_TOKEN]: new HTTPOAuth2RefreshTokenGrant({\n accessTokenIssuer: ctx.accessTokenIssuer,\n refreshTokenIssuer: ctx.refreshTokenIssuer,\n tokenVerifier: ctx.tokenVerifier,\n tokenRepository: ctx.tokenRepository,\n sessionTokenRepository: ctx.sessionTokenRepository,\n sessionManager: ctx.sessionManager,\n clientAuthenticator: ctx.oauth2ClientAuthenticator,\n realmRepository: ctx.realmRepository,\n eventService: ctx.eventService,\n metrics: ctx.metrics,\n logger: ctx.logger,\n options: { gracePeriod: ctx.tokenRefreshGracePeriod },\n certificateSource: ctx.certificateSource,\n }),\n };\n }\n\n // -------------------------------------------\n\n @DGet('/introspect', [])\n async getIntrospect(@DContext() event: IAppEvent): Promise<OAuth2TokenIntrospectionResponse> {\n return this.postIntrospect(event);\n }\n\n @DPost('/introspect', [])\n async postIntrospect(\n @DContext() event: IAppEvent,\n ): Promise<OAuth2TokenIntrospectionResponse> {\n try {\n const token = await extractTokenFromRequest(event);\n const payload = await this.tokenVerifier.verify(token, { skipActiveCheck: true });\n if (!payload.sub || !payload.sub_kind) {\n throw OAuth2RequestError.identityInvalid();\n }\n\n // todo: only receive client specific permissions\n const permissions = await this.identityPermissionProvider.getFor({\n id: payload.sub,\n type: payload.sub_kind,\n clientId: payload.client_id,\n realmId: payload.realm_id,\n });\n\n const identity = await this.identityResolver.resolve(payload.sub_kind, payload.sub);\n if (!identity) {\n // todo: differentiate between client & user\n throw OAuth2RequestError.identityInvalid();\n }\n\n const claimsBuilder = new OAuth2OpenIDClaimsBuilder();\n const claims = claimsBuilder.fromIdentity(identity);\n\n let active : boolean;\n if (payload.jti) {\n const isInactive = await this.tokenVerifier.isInactive(payload.jti);\n active = !isInactive;\n } else {\n active = false;\n }\n\n return {\n active,\n // todo: permissions property should be removed.\n permissions: Object.values(\n permissions.reduce((acc, binding) => {\n const key = buildPermissionKey(binding.permission);\n if (!acc[key]) {\n acc[key] = {\n name: binding.permission.name,\n client_id: binding.permission.clientId,\n realm_id: binding.permission.realmId,\n } as OAuth2TokenPermission;\n }\n return acc;\n }, {} as Record<string, OAuth2TokenPermission>),\n ),\n ...payload,\n ...claims,\n };\n } catch (e) {\n throw toOAuth2Error(e);\n }\n }\n\n // ----------------------------------------------------------\n\n @DPost('/revoke', [])\n async revokeToken(\n @DContext() event: IAppEvent,\n ): Promise<null> {\n try {\n const token = await extractTokenFromRequest(event);\n\n // RFC 7009 §2.2: revoking an invalid token — here one that is expired\n // or already inactive — MUST still succeed. The client cannot act on\n // an error, and the token's invalidation (the point of the request)\n // is already achieved. Signature + kind still anchor the token, so a\n // forged/garbage token is rejected; `ignoreExpiry` additionally keeps\n // this exp-bypass out of the shared claims cache (see\n // OAuth2TokenVerifier). Without this, revoking a stale refresh token\n // threw `expired_token`, so the caller's revoke-then-clear-cookie\n // flow aborted, the durable row was never soft-revoked, and the stale\n // refresh cookie survived into the next login.\n const payload = await this.tokenVerifier.verify(token, {\n ignoreExpiry: true,\n skipActiveCheck: true,\n });\n\n await this.tokenRevoker.revoke(payload);\n\n event.response.status = 202;\n return null;\n } catch (e) {\n throw toOAuth2Error(e);\n }\n }\n\n // ----------------------------------------------------------\n\n @DPost('', [])\n async createToken(@DContext() event: IAppEvent): Promise<OAuth2TokenGrantResponse> {\n const grantType = await guessOauth2GrantTypeByRequest(event);\n if (!grantType) {\n throw OAuth2GrantTypeError.unsupported();\n }\n\n const grant = this.tokenGrants[grantType];\n if (!grant) {\n throw OAuth2GrantTypeError.unsupported();\n }\n\n const response = await grant.runWithRequest(event);\n\n this.metrics?.recordTokenGrant(grantType);\n\n return response;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"module.mjs","names":["OAuth2GrantTypeError","OAuth2RequestError","OAuth2TokenGrant","isJWTError","DContext","DController","DGet","DPost","DTags","buildPermissionKey","toOAuth2Error","OAuth2OpenIDClaimsBuilder","HTTPClientCredentialsGrant","HTTPOAuth2AuthorizeGrant","HTTPOAuth2RefreshTokenGrant","HTTPPasswordGrant","guessOauth2GrantTypeByRequest","extractTokenFromRequest","TokenController","refreshTokenIssuer","accessTokenIssuer","tokenVerifier","tokenRevoker","identityResolver","identityPermissionProvider","metrics","tokenGrants","ctx","AUTHORIZATION_CODE","codeVerifier","clientAuthenticator","oauth2ClientAuthenticator","openIdTokenIssuer","keyStore","sessionManager","realmRepository","accessPolicyEvaluator","certificateSource","CLIENT_CREDENTIALS","PASSWORD","authenticator","userAuthenticator","eventService","loginThrottleService","userAuthenticatorService","mfaLoginService","logger","REFRESH_TOKEN","tokenRepository","sessionTokenRepository","options","gracePeriod","tokenRefreshGracePeriod","getIntrospect","event","postIntrospect","token","payload","verify","ignoreExpiry","skipActiveCheck","sub","sub_kind","identityInvalid","active","jti","exp","isInactive","Date","now","identity","resolve","claimsBuilder","claims","fromIdentity","permissions","getFor","id","type","clientId","client_id","realmId","realm_id","Object","values","reduce","acc","binding","key","permission","name","e","revokeToken","revoke","response","status","createToken","grantType","unsupported","grant","runWithRequest","recordTokenGrant"],"sources":["../../../../../../src/adapters/http/controllers/workflows/token/module.ts"],"sourcesContent":["/*\n * Copyright (c) 2022.\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 { OAuth2TokenGrantResponse, OAuth2TokenIntrospectionResponse, OAuth2TokenPermission } from '@authup/specs';\nimport { \n OAuth2GrantTypeError, \n OAuth2RequestError, \n OAuth2TokenGrant, \n isJWTError, \n} from '@authup/specs';\nimport {\n DContext,\n DController,\n DGet,\n DPost,\n DTags,\n} from '@routup/decorators';\nimport type { IAppEvent } from 'routup';\nimport { buildPermissionKey } from '@authup/access';\nimport { toOAuth2Error } from '../../../../../core/oauth2/helpers/index.ts';\nimport type { TokenControllerContext } from './types.ts';\nimport type {\n IAuthFlowMetrics,\n IIdentityPermissionProvider,\n IIdentityResolver,\n IOAuth2TokenIssuer,\n IOAuth2TokenRevoker,\n IOAuth2TokenVerifier,\n} from '../../../../../core/index.ts';\nimport { OAuth2OpenIDClaimsBuilder } from '../../../../../core/index.ts';\nimport type { IHTTPOAuth2Grant } from '../../../adapters/index.ts';\nimport {\n HTTPClientCredentialsGrant,\n HTTPOAuth2AuthorizeGrant,\n HTTPOAuth2RefreshTokenGrant,\n HTTPPasswordGrant,\n guessOauth2GrantTypeByRequest,\n} from '../../../adapters/index.ts';\nimport { extractTokenFromRequest } from './utils/index.ts';\n\n@DTags('auth')\n@DController('/token')\nexport class TokenController {\n protected refreshTokenIssuer: IOAuth2TokenIssuer;\n\n protected accessTokenIssuer: IOAuth2TokenIssuer;\n\n protected tokenVerifier: IOAuth2TokenVerifier;\n\n protected tokenRevoker: IOAuth2TokenRevoker;\n\n protected identityResolver : IIdentityResolver;\n\n protected identityPermissionProvider : IIdentityPermissionProvider;\n\n protected metrics? : IAuthFlowMetrics;\n\n protected tokenGrants : Record<`${OAuth2TokenGrant}`, IHTTPOAuth2Grant>;\n\n // -------------------------------------------\n\n constructor(ctx: TokenControllerContext) {\n this.refreshTokenIssuer = ctx.refreshTokenIssuer;\n this.accessTokenIssuer = ctx.accessTokenIssuer;\n this.tokenVerifier = ctx.tokenVerifier;\n this.tokenRevoker = ctx.tokenRevoker;\n this.identityResolver = ctx.identityResolver;\n this.identityPermissionProvider = ctx.identityPermissionProvider;\n this.metrics = ctx.metrics;\n\n this.tokenGrants = {\n [OAuth2TokenGrant.AUTHORIZATION_CODE]: new HTTPOAuth2AuthorizeGrant({\n codeVerifier: ctx.codeVerifier,\n clientAuthenticator: ctx.oauth2ClientAuthenticator,\n accessTokenIssuer: ctx.accessTokenIssuer,\n refreshTokenIssuer: ctx.refreshTokenIssuer,\n openIdTokenIssuer: ctx.openIdTokenIssuer,\n keyStore: ctx.keyStore,\n sessionManager: ctx.sessionManager,\n realmRepository: ctx.realmRepository,\n accessPolicyEvaluator: ctx.accessPolicyEvaluator,\n certificateSource: ctx.certificateSource,\n }),\n [OAuth2TokenGrant.CLIENT_CREDENTIALS]: new HTTPClientCredentialsGrant({\n accessTokenIssuer: ctx.accessTokenIssuer,\n clientAuthenticator: ctx.oauth2ClientAuthenticator,\n sessionManager: ctx.sessionManager,\n certificateSource: ctx.certificateSource,\n }),\n [OAuth2TokenGrant.PASSWORD]: new HTTPPasswordGrant({\n accessTokenIssuer: ctx.accessTokenIssuer,\n refreshTokenIssuer: ctx.refreshTokenIssuer,\n authenticator: ctx.userAuthenticator,\n clientAuthenticator: ctx.oauth2ClientAuthenticator,\n sessionManager: ctx.sessionManager,\n realmRepository: ctx.realmRepository,\n eventService: ctx.eventService,\n metrics: ctx.metrics,\n loginThrottleService: ctx.loginThrottleService,\n userAuthenticatorService: ctx.userAuthenticatorService,\n mfaLoginService: ctx.mfaLoginService,\n logger: ctx.logger,\n certificateSource: ctx.certificateSource,\n }),\n [OAuth2TokenGrant.REFRESH_TOKEN]: new HTTPOAuth2RefreshTokenGrant({\n accessTokenIssuer: ctx.accessTokenIssuer,\n refreshTokenIssuer: ctx.refreshTokenIssuer,\n tokenVerifier: ctx.tokenVerifier,\n tokenRepository: ctx.tokenRepository,\n sessionTokenRepository: ctx.sessionTokenRepository,\n sessionManager: ctx.sessionManager,\n clientAuthenticator: ctx.oauth2ClientAuthenticator,\n realmRepository: ctx.realmRepository,\n eventService: ctx.eventService,\n metrics: ctx.metrics,\n logger: ctx.logger,\n options: { gracePeriod: ctx.tokenRefreshGracePeriod },\n certificateSource: ctx.certificateSource,\n }),\n };\n }\n\n // -------------------------------------------\n\n @DGet('/introspect', [])\n async getIntrospect(@DContext() event: IAppEvent): Promise<OAuth2TokenIntrospectionResponse> {\n return this.postIntrospect(event);\n }\n\n @DPost('/introspect', [])\n async postIntrospect(\n @DContext() event: IAppEvent,\n ): Promise<OAuth2TokenIntrospectionResponse> {\n try {\n const token = await extractTokenFromRequest(event);\n\n // `ignoreExpiry`, so an expired token still reports WHOSE it was:\n // a third-party app can say \"your session ended, <name>\" instead\n // of only \"no\". Everything the caller could act on - the subject,\n // its claims - is in a token we did issue and can still read.\n //\n // `active` is derived below and NEVER from this call. The\n // signature-keyed cache returns a hit without re-checking `exp`\n // (verifier/module.ts), so relying on the verify to reject an\n // expired token would report one as active for as long as its\n // entry survives. The verifier deliberately skips WRITING the\n // cache on this path, so no expired payload can enter it here.\n const payload = await this.tokenVerifier.verify(token, {\n ignoreExpiry: true,\n skipActiveCheck: true,\n });\n if (!payload.sub || !payload.sub_kind) {\n // Not a report about a token this server ever issued: authup\n // mints every one with a subject, so a verifying token without\n // one was never valid. `active: false` would claim we knew it\n // and let it lapse.\n throw OAuth2RequestError.identityInvalid();\n }\n\n // Both halves, fail-closed on a missing claim: a token carrying no\n // `jti` cannot be checked against the revocation list, and one\n // carrying no `exp` cannot be shown to be current. Authup mints\n // every token with both. Derived BEFORE the permission read so a\n // dead token never pays for one.\n let active : boolean;\n if (payload.jti && typeof payload.exp === 'number') {\n const isInactive = await this.tokenVerifier.isInactive(payload.jti);\n active = !isInactive && payload.exp * 1000 > Date.now();\n } else {\n active = false;\n }\n\n const identity = await this.identityResolver.resolve(payload.sub_kind, payload.sub);\n if (!identity) {\n // todo: differentiate between client & user\n throw OAuth2RequestError.identityInvalid();\n }\n\n const claimsBuilder = new OAuth2OpenIDClaimsBuilder();\n const claims = claimsBuilder.fromIdentity(identity);\n\n // An inactive token reports WHO it belonged to and nothing about\n // what they may do. The endpoint requires no authorization (#3489),\n // so anyone holding a lapsed token string - out of a log, browser\n // history, a proxy trace - reaches this answer; naming the subject\n // is the point of reading an expired token at all, handing over\n // their authorization set is not. It also skips resolving that set\n // for a token nobody can use.\n if (!active) {\n return {\n active,\n ...payload,\n ...claims,\n };\n }\n\n // todo: only receive client specific permissions\n const permissions = await this.identityPermissionProvider.getFor({\n id: payload.sub,\n type: payload.sub_kind,\n clientId: payload.client_id,\n realmId: payload.realm_id,\n });\n\n return {\n active,\n // todo: permissions property should be removed.\n permissions: Object.values(\n permissions.reduce((acc, binding) => {\n const key = buildPermissionKey(binding.permission);\n if (!acc[key]) {\n acc[key] = {\n name: binding.permission.name,\n client_id: binding.permission.clientId,\n realm_id: binding.permission.realmId,\n } as OAuth2TokenPermission;\n }\n return acc;\n }, {} as Record<string, OAuth2TokenPermission>),\n ),\n ...payload,\n ...claims,\n };\n } catch (e) {\n // RFC 7662 §2.2: a token that \"is not active, does not exist on\n // this server, or the protected resource is not allowed to\n // introspect\" MUST be answered with `active: false`. A token this\n // server cannot read does not exist on it, so it is reported like\n // any other rather than raised - which is what §2.2 asks for and\n // what keeps this endpoint from telling a caller whether a string\n // was signed by a key we hold.\n //\n // The report is BARE, no payload: §2.2 and §4 both say not to\n // disclose more about an inactive token, and there is nothing to\n // disclose here anyway. The expired case is the deliberate\n // exception, and it never reaches this catch - it is verified\n // above and reported with its claims.\n if (isJWTError(e)) {\n return { active: false };\n }\n\n throw toOAuth2Error(e);\n }\n }\n\n // ----------------------------------------------------------\n\n @DPost('/revoke', [])\n async revokeToken(\n @DContext() event: IAppEvent,\n ): Promise<null> {\n try {\n const token = await extractTokenFromRequest(event);\n\n // RFC 7009 §2.2: revoking an invalid token — here one that is expired\n // or already inactive — MUST still succeed. The client cannot act on\n // an error, and the token's invalidation (the point of the request)\n // is already achieved. Signature + kind still anchor the token, so a\n // forged/garbage token is rejected; `ignoreExpiry` additionally keeps\n // this exp-bypass out of the shared claims cache (see\n // OAuth2TokenVerifier). Without this, revoking a stale refresh token\n // threw `expired_token`, so the caller's revoke-then-clear-cookie\n // flow aborted, the durable row was never soft-revoked, and the stale\n // refresh cookie survived into the next login.\n const payload = await this.tokenVerifier.verify(token, {\n ignoreExpiry: true,\n skipActiveCheck: true,\n });\n\n await this.tokenRevoker.revoke(payload);\n\n // RFC 7009 §2.2 names 200 for a successful revocation. This was a\n // 202 - within the 2xx family, so a client reading the class was\n // unaffected, but not the status the spec asks for.\n event.response.status = 200;\n return null;\n } catch (e) {\n // Same clause, the other half: \"invalid tokens do not cause an\n // error response since the client cannot handle such an error in a\n // reasonable way\". Expiry is already bypassed above, so this covers\n // a malformed or unverifiable token, which answered 401/404. The\n // answer is the one a valid token gets - the point is that the two\n // are indistinguishable.\n if (isJWTError(e)) {\n event.response.status = 200;\n return null;\n }\n\n throw toOAuth2Error(e);\n }\n }\n\n // ----------------------------------------------------------\n\n @DPost('', [])\n async createToken(@DContext() event: IAppEvent): Promise<OAuth2TokenGrantResponse> {\n const grantType = await guessOauth2GrantTypeByRequest(event);\n if (!grantType) {\n throw OAuth2GrantTypeError.unsupported();\n }\n\n const grant = this.tokenGrants[grantType];\n if (!grant) {\n throw OAuth2GrantTypeError.unsupported();\n }\n\n const response = await grant.runWithRequest(event);\n\n this.metrics?.recordTokenGrant(grantType);\n\n return response;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
+
import { defineIssueItem } from "../../../../../../node_modules/@ebec/core/dist/index.mjs";
|
|
4
5
|
import { readFromLocations } from "../../../../request/helpers/locations.mjs";
|
|
5
6
|
import { useRequestToken } from "../../../../request/helpers/token.mjs";
|
|
6
7
|
import "../../../../request/index.mjs";
|
|
7
8
|
import { TokenRequestValidator } from "./validator.mjs";
|
|
8
|
-
import { ValidupError, buildErrorMessageForAttribute,
|
|
9
|
+
import { ValidupError, buildErrorMessageForAttribute, isValidupError } from "validup";
|
|
9
10
|
//#region src/adapters/http/controllers/workflows/token/utils/extract.ts
|
|
10
11
|
async function extractTokenFromRequest(event) {
|
|
11
12
|
let token;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extract.mjs","names":["ValidupError","buildErrorMessageForAttribute","
|
|
1
|
+
{"version":3,"file":"extract.mjs","names":["ValidupError","buildErrorMessageForAttribute","isValidupError","defineIssueItem","readFromLocations","useRequestToken","TokenRequestValidator","extractTokenFromRequest","event","token","validator","data","run","e","path","message"],"sources":["../../../../../../../src/adapters/http/controllers/workflows/token/utils/extract.ts"],"sourcesContent":["/*\n * Copyright (c) 2024.\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 { IAppEvent } from 'routup';\nimport {\n ValidupError,\n buildErrorMessageForAttribute,\n isValidupError,\n} from 'validup';\nimport { defineIssueItem } from '@ebec/core';\nimport { readFromLocations, useRequestToken } from '../../../../request/index.ts';\nimport { TokenRequestValidator } from './validator.ts';\n\nexport async function extractTokenFromRequest(event: IAppEvent) : Promise<string> {\n let token : string | undefined;\n\n try {\n const validator = new TokenRequestValidator();\n const data = await validator.run(\n await readFromLocations(event, ['body', 'query', 'params']),\n );\n\n token = data.token;\n } catch (e) {\n if (!isValidupError(e)) {\n throw e;\n }\n token = useRequestToken(event);\n }\n\n if (!token) {\n throw new ValidupError([\n defineIssueItem({\n path: ['token'],\n message: buildErrorMessageForAttribute('token'),\n }),\n ]);\n }\n\n return token;\n}\n"],"mappings":";;;;;;;;;;AAiBA,eAAsBO,wBAAwBC,OAAgB;CAC1D,IAAIC;CAEJ,IAAI;EAMAA,SAAQE,MAJWD,IADGJ,sBACHI,CAAAA,CAAUE,IACzB,MAAMR,kBAAkBI,OAAO;GAAC;GAAQ;GAAS;EAAS,CAAA,CAAA,EAAA,CAGjDC;CACjB,SAASI,GAAG;EACR,IAAI,CAACX,eAAeW,CAAAA,GAChB,MAAMA;EAEVJ,QAAQJ,gBAAgBG,KAAAA;CAC5B;CAEA,IAAI,CAACC,OACD,MAAM,IAAIT,aAAa,CACnBG,gBAAgB;EACZW,MAAM,CAAC,OAAQ;EACfC,SAASd,8BAA8B,OAAA;CAC3C,CAAA,CACH,CAAA;CAGL,OAAOQ;AACX"}
|
|
@@ -12,9 +12,9 @@ import { registerThemeMiddleware } from "./theme.mjs";
|
|
|
12
12
|
import { resolveAccountConsoleDistPath } from "../../ui/account-console/module.mjs";
|
|
13
13
|
import "../../ui/account-console/index.mjs";
|
|
14
14
|
import { InternalError } from "@authup/errors";
|
|
15
|
+
import { CodeTransformation, isCodeTransformation } from "typeorm-extension";
|
|
15
16
|
import path from "node:path";
|
|
16
17
|
import { defineCoreHandler } from "routup";
|
|
17
|
-
import { CodeTransformation, isCodeTransformation } from "typeorm-extension";
|
|
18
18
|
import { fromNodeMiddleware } from "routup/node";
|
|
19
19
|
import { read } from "locter";
|
|
20
20
|
import fs from "node:fs";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../../../../../src/adapters/http/middleware/built-in/cors.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAElC,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,WAAW,
|
|
1
|
+
{"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../../../../../src/adapters/http/middleware/built-in/cors.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAElC,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,WAAW,QAmCtE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cors.mjs","names":["cors","registerCorsMiddleware","router","input","use","origin","credentials","exposeHeaders"],"sources":["../../../../../src/adapters/http/middleware/built-in/cors.ts"],"sourcesContent":["/*\n * Copyright (c) 2023.\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 { Options as CorsOptions } from '@routup/cors';\nimport { cors } from '@routup/cors';\nimport type { App } from 'routup';\n\nexport function registerCorsMiddleware(router: App, input?: CorsOptions) {\n router.use(cors({\n // Reflect any origin by default: OAuth2 clients (and their UIs) are\n // registered at runtime on arbitrary domains, so a startup-time\n // allowlist cannot know them.
|
|
1
|
+
{"version":3,"file":"cors.mjs","names":["cors","registerCorsMiddleware","router","input","use","origin","credentials","exposeHeaders"],"sources":["../../../../../src/adapters/http/middleware/built-in/cors.ts"],"sourcesContent":["/*\n * Copyright (c) 2023.\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 { Options as CorsOptions } from '@routup/cors';\nimport { cors } from '@routup/cors';\nimport type { App } from 'routup';\n\nexport function registerCorsMiddleware(router: App, input?: CorsOptions) {\n router.use(cors({\n // Reflect any origin by default: OAuth2 clients (and their UIs) are\n // registered at runtime on arbitrary domains, so a startup-time\n // allowlist cannot know them. Authentication is header-based\n // (Bearer/Basic), so an origin allowlist would add no security here,\n // only break dynamically registered browser clients. Operators can\n // still pass an explicit `origin` via the middlewareCors config\n // options.\n //\n // ONE endpoint authenticates on a cookie: the federated login's\n // `POST /identity-providers/:id/login-complete` (plan 094). Reflecting\n // an origin WITH credentials would let any same-site origin read the\n // token pair it answers with, since `SameSite` is scoped to the\n // registrable domain rather than the origin, so that route checks\n // `Origin` against publicUrl itself. A second cookie-authenticated\n // endpoint must do the same, or this default has to change.\n origin: true,\n credentials: true,\n // `credentials: true` is incompatible with the `*` wildcard for\n // exposeHeaders, so enumerate the response headers JS clients need.\n exposeHeaders: [\n 'ratelimit-limit',\n 'ratelimit-remaining',\n 'ratelimit-reset',\n 'retry-after',\n 'etag',\n 'content-disposition',\n 'content-range',\n 'accept-ranges',\n 'location',\n 'www-authenticate',\n ],\n ...(input ?? {}),\n }));\n}\n"],"mappings":";;;;;AAWA,SAAgBC,uBAAuBC,QAAaC,OAAmB;CACnED,OAAOE,IAAIJ,KAAK;EAgBZK,QAAQ;EACRC,aAAa;EAGbC,eAAe;GACX;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACH;EACD,GAAIJ,SAAS,CAAC;CAClB,CAAA,CAAA;AACJ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../../../../src/adapters/http/middleware/built-in/error.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAElC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../../../../src/adapters/http/middleware/built-in/error.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAElC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAKjD,KAAK,sBAAsB,GAAG;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAA;CAClB,CAAC;AAEF,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,GAAE,sBAA2B,QAyDxF"}
|
|
@@ -3,7 +3,8 @@ import "node:url";
|
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { describeError, sanitizeError } from "../../../../utils/error.mjs";
|
|
5
5
|
import "../../../../utils/index.mjs";
|
|
6
|
-
import { httpStatusFromCode, serializeError } from "@authup/errors";
|
|
6
|
+
import { ErrorCode, httpStatusFromCode, serializeError } from "@authup/errors";
|
|
7
|
+
import { isJWTErrorCode } from "@authup/specs";
|
|
7
8
|
import { defineErrorHandler } from "routup";
|
|
8
9
|
//#region src/adapters/http/middleware/built-in/error.ts
|
|
9
10
|
function registerErrorMiddleware(router, options = {}) {
|
|
@@ -16,6 +17,14 @@ function registerErrorMiddleware(router, options = {}) {
|
|
|
16
17
|
if (options.logger) options.logger.error(describeError(original));
|
|
17
18
|
payload.message = "An internal server error occurred.";
|
|
18
19
|
}
|
|
20
|
+
if (status === 401) {
|
|
21
|
+
const authorization = event.headers.get("authorization");
|
|
22
|
+
if (typeof authorization === "string" && authorization.toLowerCase().startsWith("bearer ")) {
|
|
23
|
+
const description = String(payload.message ?? "").replace(/[^\x20-\x21\x23-\x5B\x5D-\x7E]/g, "");
|
|
24
|
+
const code = isJWTErrorCode(next.code) ? "invalid_token" : "invalid_request";
|
|
25
|
+
event.response.headers.set("www-authenticate", `Bearer error="${code}", error_description="${description}"`);
|
|
26
|
+
} else if (!authorization && next.code === ErrorCode.IDENTITY_UNAUTHORIZED) event.response.headers.set("www-authenticate", "Bearer");
|
|
27
|
+
}
|
|
19
28
|
event.response.status = status;
|
|
20
29
|
return payload;
|
|
21
30
|
}));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error.mjs","names":["defineErrorHandler","httpStatusFromCode","serializeError","describeError","sanitizeError","registerErrorMiddleware","router","options","use","error","event","original","cause","next","status","code","payload","logger","message","response"],"sources":["../../../../../src/adapters/http/middleware/built-in/error.ts"],"sourcesContent":["/*\n * Copyright (c) 2021-2023.\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 { App } from 'routup';\nimport { defineErrorHandler } from 'routup';\nimport type { Logger } from '@authup/server-kit';\nimport { httpStatusFromCode, serializeError } from '@authup/errors';\nimport { describeError, sanitizeError } from '../../../../utils/index.ts';\n\ntype ErrorMiddlewareOptions = {\n logger?: Logger\n};\n\nexport function registerErrorMiddleware(router: App, options: ErrorMiddlewareOptions = {}) {\n router.use(defineErrorHandler((error, event) => {\n // routup wraps whatever a handler threw into an AppError carrying it\n // as `cause`, so this is the error as it was actually raised.\n const original = error.cause ?? error;\n\n const next = sanitizeError(original);\n const status = httpStatusFromCode(next.code);\n\n const payload = serializeError(next);\n\n if (status >= 500) {\n if (options.logger) {\n // The ORIGINAL error, never the sanitized copy. Sanitizing\n // discards the transport reason and the upstream response\n // body on purpose, and those are the only things that make\n // an outbound failure diagnosable.\n options.logger.error(describeError(original));\n }\n payload.message = 'An internal server error occurred.';\n }\n\n event.response.status = status;\n return payload;\n }));\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"error.mjs","names":["defineErrorHandler","ErrorCode","httpStatusFromCode","serializeError","isJWTErrorCode","describeError","sanitizeError","registerErrorMiddleware","router","options","use","error","event","original","cause","next","status","code","payload","logger","message","authorization","headers","get","toLowerCase","startsWith","description","String","replace","response","set","IDENTITY_UNAUTHORIZED"],"sources":["../../../../../src/adapters/http/middleware/built-in/error.ts"],"sourcesContent":["/*\n * Copyright (c) 2021-2023.\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 { App } from 'routup';\nimport { defineErrorHandler } from 'routup';\nimport type { Logger } from '@authup/server-kit';\nimport { ErrorCode, httpStatusFromCode, serializeError } from '@authup/errors';\nimport { isJWTErrorCode } from '@authup/specs';\nimport { describeError, sanitizeError } from '../../../../utils/index.ts';\n\ntype ErrorMiddlewareOptions = {\n logger?: Logger\n};\n\nexport function registerErrorMiddleware(router: App, options: ErrorMiddlewareOptions = {}) {\n router.use(defineErrorHandler((error, event) => {\n // routup wraps whatever a handler threw into an AppError carrying it\n // as `cause`, so this is the error as it was actually raised.\n const original = error.cause ?? error;\n\n const next = sanitizeError(original);\n const status = httpStatusFromCode(next.code);\n\n const payload = serializeError(next);\n\n if (status >= 500) {\n if (options.logger) {\n // The ORIGINAL error, never the sanitized copy. Sanitizing\n // discards the transport reason and the upstream response\n // body on purpose, and those are the only things that make\n // an outbound failure diagnosable.\n options.logger.error(describeError(original));\n }\n payload.message = 'An internal server error occurred.';\n }\n\n // RFC 6750 §3: a 401 from a protected resource carries the Bearer\n // challenge, which nothing here emitted. Gated on the request actually\n // being one - the token endpoint answers 401 for a bad client secret\n // (RFC 6749 §5.2) and that is not a bearer failure, so it must not be\n // stamped.\n if (status === 401) {\n const authorization = event.headers.get('authorization');\n\n if (typeof authorization === 'string' && authorization.toLowerCase().startsWith('bearer ')) {\n // RFC 6750 §3 defines the value as a quoted-string over\n // `%x20-21 / %x23-5B / %x5D-7E`, so everything outside that set\n // goes: a quote or a backslash would break out of the\n // quoting, and a control character (a CR or LF in a\n // third-party JWT library's\n // message, which `JWTError.payloadInvalid` forwards verbatim)\n // makes the header assignment itself throw, turning a 401\n // into a 500.\n const description = String(payload.message ?? '')\n .replace(/[^\\x20-\\x21\\x23-\\x5B\\x5D-\\x7E]/g, '');\n const code = isJWTErrorCode(next.code) ? 'invalid_token' : 'invalid_request';\n\n event.response.headers.set(\n 'www-authenticate',\n `Bearer error=\"${code}\", error_description=\"${description}\"`,\n );\n } else if (!authorization && next.code === ErrorCode.IDENTITY_UNAUTHORIZED) {\n // §3 again: a request that presented no credentials at all gets\n // the bare challenge, with no error code to report.\n event.response.headers.set('www-authenticate', 'Bearer');\n }\n }\n\n event.response.status = status;\n return payload;\n }));\n}\n"],"mappings":";;;;;;;;;AAkBA,SAAgBO,wBAAwBC,QAAaC,UAAkC,CAAC,GAAC;CACrFD,OAAOE,IAAIV,oBAAoBW,OAAOC,UAAAA;EAGlC,MAAMC,WAAWF,MAAMG,SAASH;EAEhC,MAAMI,OAAOT,cAAcO,QAAAA;EAC3B,MAAMG,SAASd,mBAAmBa,KAAKE,IAAI;EAE3C,MAAMC,UAAUf,eAAeY,IAAAA;EAE/B,IAAIC,UAAU,KAAK;GACf,IAAIP,QAAQU,QAKRV,QAAQU,OAAOR,MAAMN,cAAcQ,QAAAA,CAAAA;GAEvCK,QAAQE,UAAU;EACtB;EAOA,IAAIJ,WAAW,KAAK;GAChB,MAAMK,gBAAgBT,MAAMU,QAAQC,IAAI,eAAA;GAExC,IAAI,OAAOF,kBAAkB,YAAYA,cAAcG,YAAW,CAAA,CAAGC,WAAW,SAAA,GAAY;IASxF,MAAMC,cAAcC,OAAOT,QAAQE,WAAW,EAAA,CAAA,CACzCQ,QAAQ,mCAAmC,EAAA;IAChD,MAAMX,OAAOb,eAAeW,KAAKE,IAAI,IAAI,kBAAkB;IAE3DL,MAAMiB,SAASP,QAAQQ,IACnB,oBACA,iBAAiBb,KAAK,wBAAwBS,YAAY,EAAE;GAEpE,OAAO,IAAI,CAACL,iBAAiBN,KAAKE,SAAShB,UAAU8B,uBAGjDnB,MAAMiB,SAASP,QAAQQ,IAAI,oBAAoB,QAAA;EAEvD;EAEAlB,MAAMiB,SAASb,SAASA;EACxB,OAAOE;CACX,CAAA,CAAA;AACJ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"param-id.d.ts","sourceRoot":"","sources":["../../../../../src/adapters/http/request/helpers/param-id.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"param-id.d.ts","sourceRoot":"","sources":["../../../../../src/adapters/http/request/helpers/param-id.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAIxC,KAAK,qBAAqB,GAAG;IACzB;;OAEG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACnB,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,GAAE,qBAA0B,GAAI,MAAM,CAYhG;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,GAAE,qBAA0B,GAAI,MAAM,GAAG,SAAS,CAY5G;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,GAAI,MAAM,GAAG,SAAS,CAOxF;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,GAAI,MAAM,GAAG,SAAS,CAY9F"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
+
import { defineIssueItem } from "../../../../node_modules/@ebec/core/dist/index.mjs";
|
|
4
5
|
import { isUUID } from "@authup/kit";
|
|
5
|
-
import { ValidupError, buildErrorMessageForAttribute
|
|
6
|
+
import { ValidupError, buildErrorMessageForAttribute } from "validup";
|
|
6
7
|
//#region src/adapters/http/request/helpers/param-id.ts
|
|
7
8
|
function useRequestParamID(event, options = {}) {
|
|
8
9
|
const id = getRequestParamID(event, options);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"param-id.mjs","names":["isUUID","ValidupError","buildErrorMessageForAttribute","defineIssueItem","useRequestParamID","event","options","id","getRequestParamID","path","message","getRequestStringParam","undefined","key","value","params","length","getRequestStringParamOrFail"],"sources":["../../../../../src/adapters/http/request/helpers/param-id.ts"],"sourcesContent":["/*\n * Copyright (c) 2024.\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 { isUUID } from '@authup/kit';\nimport type { IAppEvent } from 'routup';\nimport { ValidupError, buildErrorMessageForAttribute
|
|
1
|
+
{"version":3,"file":"param-id.mjs","names":["isUUID","ValidupError","buildErrorMessageForAttribute","defineIssueItem","useRequestParamID","event","options","id","getRequestParamID","path","message","getRequestStringParam","undefined","key","value","params","length","getRequestStringParamOrFail"],"sources":["../../../../../src/adapters/http/request/helpers/param-id.ts"],"sourcesContent":["/*\n * Copyright (c) 2024.\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 { isUUID } from '@authup/kit';\nimport type { IAppEvent } from 'routup';\nimport { ValidupError, buildErrorMessageForAttribute } from 'validup';\nimport { defineIssueItem } from '@ebec/core';\n\ntype RequestIDParamOptions = {\n /**\n * default: true\n */\n isUUID?: boolean\n};\n\nexport function useRequestParamID(event: IAppEvent, options: RequestIDParamOptions = {}) : string {\n const id = getRequestParamID(event, options);\n if (typeof id === 'undefined') {\n throw new ValidupError([\n defineIssueItem({\n path: ['id'],\n message: buildErrorMessageForAttribute('id'),\n }),\n ]);\n }\n\n return id;\n}\n\nexport function getRequestParamID(event: IAppEvent, options: RequestIDParamOptions = {}) : string | undefined {\n const id = getRequestStringParam(event, 'id');\n if (!id) {\n return undefined;\n }\n\n options.isUUID = options.isUUID ?? true;\n if (options.isUUID && !isUUID(id)) {\n return undefined;\n }\n\n return id;\n}\n\nexport function getRequestStringParam(event: IAppEvent, key: string) : string | undefined {\n const value = event.params[key];\n if (typeof value !== 'string' || value.length === 0) {\n return undefined;\n }\n\n return value;\n}\n\nexport function getRequestStringParamOrFail(event: IAppEvent, key: string) : string | undefined {\n const value = getRequestStringParam(event, key);\n if (typeof value === 'undefined') {\n throw new ValidupError([\n defineIssueItem({\n path: [key],\n message: buildErrorMessageForAttribute(key),\n }),\n ]);\n }\n\n return value;\n}\n"],"mappings":";;;;;;;AAmBA,SAAgBI,kBAAkBC,OAAkBC,UAAiC,CAAC,GAAC;CACnF,MAAMC,KAAKC,kBAAkBH,OAAOC,OAAAA;CACpC,IAAI,OAAOC,OAAO,aACd,MAAM,IAAIN,aAAa,CACnBE,gBAAgB;EACZM,MAAM,CAAC,IAAK;EACZC,SAASR,8BAA8B,IAAA;CAC3C,CAAA,CACH,CAAA;CAGL,OAAOK;AACX;AAEA,SAAgBC,kBAAkBH,OAAkBC,UAAiC,CAAC,GAAC;CACnF,MAAMC,KAAKI,sBAAsBN,OAAO,IAAA;CACxC,IAAI,CAACE,IACD;CAGJD,QAAQN,SAASM,QAAQN,UAAU;CACnC,IAAIM,QAAQN,UAAU,CAACA,OAAOO,EAAAA,GAC1B;CAGJ,OAAOA;AACX;AAEA,SAAgBI,sBAAsBN,OAAkBQ,KAAW;CAC/D,MAAMC,QAAQT,MAAMU,OAAOF;CAC3B,IAAI,OAAOC,UAAU,YAAYA,MAAME,WAAW,GAC9C;CAGJ,OAAOF;AACX;AAEA,SAAgBG,4BAA4BZ,OAAkBQ,KAAW;CACrE,MAAMC,QAAQH,sBAAsBN,OAAOQ,GAAAA;CAC3C,IAAI,OAAOC,UAAU,aACjB,MAAM,IAAIb,aAAa,CACnBE,gBAAgB;EACZM,MAAM,CAACI,GAAI;EACXH,SAASR,8BAA8BW,GAAAA;CAC3C,CAAA,CACH,CAAA;CAGL,OAAOC;AACX"}
|
|
@@ -11,9 +11,9 @@ import "../../middleware/index.mjs";
|
|
|
11
11
|
import { resolveAccountConsoleRef } from "./ref.mjs";
|
|
12
12
|
import { getURLBasePath } from "@authup/kit";
|
|
13
13
|
import { InternalError } from "@authup/errors";
|
|
14
|
+
import { CodeTransformation, isCodeTransformation } from "typeorm-extension";
|
|
14
15
|
import path from "node:path";
|
|
15
16
|
import { useRequestQuery } from "@routup/basic/query";
|
|
16
|
-
import { CodeTransformation, isCodeTransformation } from "typeorm-extension";
|
|
17
17
|
import { locateUpSync } from "locter";
|
|
18
18
|
import fs from "node:fs";
|
|
19
19
|
//#region src/adapters/http/ui/account-console/module.ts
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
import { Client } from "@authup/core-http-kit";
|
|
5
4
|
import { FetchTransport, fetch } from "hapic";
|
|
5
|
+
import { Client } from "@authup/core-http-kit";
|
|
6
6
|
//#region src/adapters/http/ui/auth-console/http-client.ts
|
|
7
7
|
/**
|
|
8
8
|
* Build a rewriter that maps URLs under the public base URL
|
|
@@ -12,8 +12,8 @@ import { UI_HTTP_CLIENT_FACTORY_STORE_KEY } from "../../middleware/built-in/ui-h
|
|
|
12
12
|
import "../../middleware/index.mjs";
|
|
13
13
|
import { getURLBasePath } from "@authup/kit";
|
|
14
14
|
import { InternalError, isError } from "@authup/errors";
|
|
15
|
-
import path from "node:path";
|
|
16
15
|
import { CodeTransformation, isCodeTransformation } from "typeorm-extension";
|
|
16
|
+
import path from "node:path";
|
|
17
17
|
import { read } from "locter";
|
|
18
18
|
import fs from "node:fs";
|
|
19
19
|
//#region src/adapters/http/ui/auth-console/module.ts
|
|
@@ -6,7 +6,7 @@ import type { ConsolePackageOptions } from './types.ts';
|
|
|
6
6
|
* Bump alongside `apps/client-auth-console/src/contract.ts` whenever a
|
|
7
7
|
* change breaks a package built against the previous shape.
|
|
8
8
|
*/
|
|
9
|
-
export declare const AUTH_CONSOLE_CONTRACT_VERSION =
|
|
9
|
+
export declare const AUTH_CONSOLE_CONTRACT_VERSION = 3;
|
|
10
10
|
/**
|
|
11
11
|
* The marker the account console's index.html must carry. It is that
|
|
12
12
|
* console's entire runtime contract: without it the injected
|
|
@@ -16,7 +16,7 @@ import fs from "node:fs";
|
|
|
16
16
|
*
|
|
17
17
|
* Bump alongside `apps/client-auth-console/src/contract.ts` whenever a
|
|
18
18
|
* change breaks a package built against the previous shape.
|
|
19
|
-
*/ const AUTH_CONSOLE_CONTRACT_VERSION =
|
|
19
|
+
*/ const AUTH_CONSOLE_CONTRACT_VERSION = 3;
|
|
20
20
|
/**
|
|
21
21
|
* The marker the account console's index.html must carry. It is that
|
|
22
22
|
* console's entire runtime contract: without it the injected
|
|
@@ -56,7 +56,7 @@ async function assertAuthConsoleContract(packagePath) {
|
|
|
56
56
|
}
|
|
57
57
|
if (typeof module.render !== "function") throw new AuthupError(`The auth console bundle "${entry}" does not export a render() function.`);
|
|
58
58
|
const version = typeof module.CONTRACT_VERSION === "undefined" ? 1 : module.CONTRACT_VERSION;
|
|
59
|
-
if (version !==
|
|
59
|
+
if (version !== 3) throw new AuthupError(`The auth console package at "${packagePath}" implements render-contract version ${version}, but this authup requires 3. Rebuild it against the current @authup/client-auth-console contract.`);
|
|
60
60
|
}
|
|
61
61
|
function assertAccountConsoleContract(packagePath) {
|
|
62
62
|
const distPath = resolveAccountConsoleDistPath();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module.mjs","names":["AuthupError","normalizeError","read","fs","path","resolveAccountConsoleDistPath","setAccountConsolePackagePath","resolveAuthConsoleDistPath","setAuthConsolePackagePath","AUTH_CONSOLE_CONTRACT_VERSION","AUTH_CONSOLE_CONTRACT_VERSION_IN_SYNC","ACCOUNT_CONSOLE_CONFIG_MARKER","bindConsolePackages","options","authConsolePath","accountConsolePath","assertAuthConsoleContract","assertAccountConsoleContract","packagePath","distPath","entry","join","module","e","reason","message","render","version","CONTRACT_VERSION","html","readFileSync","includes"],"sources":["../../../../../src/adapters/http/ui/console-packages/module.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 { AuthupError, normalizeError } from '@authup/errors';\nimport { read } from 'locter';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport {\n resolveAccountConsoleDistPath,\n setAccountConsolePackagePath,\n} from '../account-console/index.ts';\nimport { resolveAuthConsoleDistPath, setAuthConsolePackagePath } from '../auth-console/index.ts';\n// Type position only (see AUTH_CONSOLE_CONTRACT_VERSION_IN_SYNC below), so\n// the runtime stays a dist-file read and the layering rule holds.\nimport type { CONTRACT_VERSION as AuthConsoleContractVersion } from '@authup/client-auth-console';\nimport type { ConsolePackageOptions } from './types.ts';\n\n/**\n * The render-contract version server-core compiles against. Must match the\n * `CONTRACT_VERSION` the auth console bundle exports.\n *\n * Bump alongside `apps/client-auth-console/src/contract.ts` whenever a\n * change breaks a package built against the previous shape.\n */\nexport const AUTH_CONSOLE_CONTRACT_VERSION =
|
|
1
|
+
{"version":3,"file":"module.mjs","names":["AuthupError","normalizeError","read","fs","path","resolveAccountConsoleDistPath","setAccountConsolePackagePath","resolveAuthConsoleDistPath","setAuthConsolePackagePath","AUTH_CONSOLE_CONTRACT_VERSION","AUTH_CONSOLE_CONTRACT_VERSION_IN_SYNC","ACCOUNT_CONSOLE_CONFIG_MARKER","bindConsolePackages","options","authConsolePath","accountConsolePath","assertAuthConsoleContract","assertAccountConsoleContract","packagePath","distPath","entry","join","module","e","reason","message","render","version","CONTRACT_VERSION","html","readFileSync","includes"],"sources":["../../../../../src/adapters/http/ui/console-packages/module.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 { AuthupError, normalizeError } from '@authup/errors';\nimport { read } from 'locter';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport {\n resolveAccountConsoleDistPath,\n setAccountConsolePackagePath,\n} from '../account-console/index.ts';\nimport { resolveAuthConsoleDistPath, setAuthConsolePackagePath } from '../auth-console/index.ts';\n// Type position only (see AUTH_CONSOLE_CONTRACT_VERSION_IN_SYNC below), so\n// the runtime stays a dist-file read and the layering rule holds.\nimport type { CONTRACT_VERSION as AuthConsoleContractVersion } from '@authup/client-auth-console';\nimport type { ConsolePackageOptions } from './types.ts';\n\n/**\n * The render-contract version server-core compiles against. Must match the\n * `CONTRACT_VERSION` the auth console bundle exports.\n *\n * Bump alongside `apps/client-auth-console/src/contract.ts` whenever a\n * change breaks a package built against the previous shape.\n */\nexport const AUTH_CONSOLE_CONTRACT_VERSION = 3;\n\n/**\n * Compile-time link to the value this constant mirrors.\n *\n * The runtime assert below only fires for a SUBSTITUTED package, so a bump\n * on one side and not the other would otherwise go unnoticed until a\n * substituted console failed at boot in someone else's deployment. The\n * annotation costs nothing at runtime (types are erased) and fails this\n * package's build the moment the two drift.\n */\ntype AuthConsoleContractVersionInSync = typeof AuthConsoleContractVersion extends typeof AUTH_CONSOLE_CONTRACT_VERSION ?\n true :\n never;\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nconst AUTH_CONSOLE_CONTRACT_VERSION_IN_SYNC : AuthConsoleContractVersionInSync = true;\n\n/**\n * The marker the account console's index.html must carry. It is that\n * console's entire runtime contract: without it the injected\n * `window.__AUTHUP__` never lands and the SPA silently falls back to\n * deriving its API URL from the origin.\n */\nexport const ACCOUNT_CONSOLE_CONFIG_MARKER = '<!--account-config-->';\n\n/**\n * Bind the substituted console packages and verify they still fulfill the\n * contracts, before the first request rather than per render.\n *\n * Only runs for a package the operator actually substituted. With the\n * default resolution the console packages ship from this repo with linked\n * versions, so the assert would compare a constant against itself, and\n * loading the SSR bundle at boot would turn a missing build into a failed\n * start instead of an actionable error on the page.\n *\n * Fail-closed on a mismatch: the operator replaced the login and consent\n * IMPLEMENTATION (the prompt ladder, PKCE and state handling, MFA\n * ordering, redirect gating), so a contract drift must stop the container\n * rather than render subtly wrong auth pages.\n */\nexport async function bindConsolePackages(options: ConsolePackageOptions = {}) : Promise<void> {\n setAuthConsolePackagePath(options.authConsolePath);\n setAccountConsolePackagePath(options.accountConsolePath);\n\n if (options.authConsolePath) {\n await assertAuthConsoleContract(options.authConsolePath);\n }\n\n if (options.accountConsolePath) {\n assertAccountConsoleContract(options.accountConsolePath);\n }\n}\n\nasync function assertAuthConsoleContract(packagePath: string) : Promise<void> {\n const distPath = resolveAuthConsoleDistPath();\n if (!distPath) {\n throw new AuthupError(\n `The auth console package at \"${packagePath}\" carries no built bundle (expected dist/client/index.html).`,\n );\n }\n\n const entry = path.join(distPath, 'server', 'server.js');\n\n let module : Record<string, any>;\n try {\n module = await read(entry) as Record<string, any>;\n } catch (e) {\n const reason = normalizeError(e).message;\n\n throw new AuthupError(`The auth console render entry \"${entry}\" could not be loaded.\\n ${reason}`);\n }\n\n if (typeof module.render !== 'function') {\n throw new AuthupError(`The auth console bundle \"${entry}\" does not export a render() function.`);\n }\n\n // A bundle predating the export counts as version 1, the shape every\n // package built before the constant existed implements.\n const version = typeof module.CONTRACT_VERSION === 'undefined' ?\n 1 :\n module.CONTRACT_VERSION;\n\n if (version !== AUTH_CONSOLE_CONTRACT_VERSION) {\n throw new AuthupError(\n `The auth console package at \"${packagePath}\" implements render-contract version ${version}, but this authup requires ${AUTH_CONSOLE_CONTRACT_VERSION}. Rebuild it against the current @authup/client-auth-console contract.`,\n );\n }\n}\n\nfunction assertAccountConsoleContract(packagePath: string) : void {\n const distPath = resolveAccountConsoleDistPath();\n if (!distPath) {\n throw new AuthupError(\n `The account console package at \"${packagePath}\" carries no built bundle (expected dist/index.html).`,\n );\n }\n\n const entry = path.join(distPath, 'index.html');\n const html = fs.readFileSync(entry, 'utf-8');\n\n if (!html.includes(ACCOUNT_CONSOLE_CONFIG_MARKER)) {\n throw new AuthupError(\n `The account console shell \"${entry}\" carries no ${ACCOUNT_CONSOLE_CONFIG_MARKER} marker, so its runtime configuration cannot be injected.`,\n );\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;GA4BA,MAAaS,gCAAgC;;;;;;GAuB7C,MAAaE,gCAAgC;;;;;;;;;;;;;;;GAiB7C,eAAsBC,oBAAoBC,UAAiC,CAAC,GAAC;CACzEL,0BAA0BK,QAAQC,eAAe;CACjDR,6BAA6BO,QAAQE,kBAAkB;CAEvD,IAAIF,QAAQC,iBACR,MAAME,0BAA0BH,QAAQC,eAAe;CAG3D,IAAID,QAAQE,oBACRE,6BAA6BJ,QAAQE,kBAAkB;AAE/D;AAEA,eAAeC,0BAA0BE,aAAmB;CACxD,MAAMC,WAAWZ,2BAAAA;CACjB,IAAI,CAACY,UACD,MAAM,IAAInB,YACN,gCAAgCkB,YAAY,6DAA6D;CAIjH,MAAME,QAAQhB,KAAKiB,KAAKF,UAAU,UAAU,WAAA;CAE5C,IAAIG;CACJ,IAAI;EACAA,SAAS,MAAMpB,KAAKkB,KAAAA;CACxB,SAASG,GAAG;EACR,MAAMC,SAASvB,eAAesB,CAAAA,CAAAA,CAAGE;EAEjC,MAAM,IAAIzB,YAAY,kCAAkCoB,MAAM,4BAA4BI,QAAQ;CACtG;CAEA,IAAI,OAAOF,OAAOI,WAAW,YACzB,MAAM,IAAI1B,YAAY,4BAA4BoB,MAAM,uCAAuC;CAKnG,MAAMO,UAAU,OAAOL,OAAOM,qBAAqB,cAC/C,IACAN,OAAOM;CAEX,IAAID,YAAAA,GACA,MAAM,IAAI3B,YACN,gCAAgCkB,YAAY,uCAAuCS,QAAQ,mGAAkI;AAGzO;AAEA,SAASV,6BAA6BC,aAAmB;CACrD,MAAMC,WAAWd,8BAAAA;CACjB,IAAI,CAACc,UACD,MAAM,IAAInB,YACN,mCAAmCkB,YAAY,sDAAsD;CAI7G,MAAME,QAAQhB,KAAKiB,KAAKF,UAAU,YAAA;CAGlC,IAAI,CAFShB,GAAG2B,aAAaV,OAAO,OAE/BS,CAAAA,CAAKE,SAAAA,uBAASpB,GACf,MAAM,IAAIX,YACN,8BAA8BoB,MAAM,eAAeT,8BAA8B,0DAA0D;AAGvJ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../../../../src/adapters/http/ui/shared/html.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAKxC,MAAM,MAAM,mBAAmB,GAAG;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,SAAS,GAAI,mBAAmB,CAK9E;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,mBAAmB,GAAI,MAAM,CAU3F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAI,MAAM,CAE1F;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAI,MAAM,CAWxE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAI,MAAM,CAEvE;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,GAAI,IAAI,
|
|
1
|
+
{"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../../../../src/adapters/http/ui/shared/html.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAKxC,MAAM,MAAM,mBAAmB,GAAG;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,SAAS,GAAI,mBAAmB,CAK9E;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,mBAAmB,GAAI,MAAM,CAU3F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAI,MAAM,CAE1F;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAI,MAAM,CAWxE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAI,MAAM,CAEvE;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,GAAI,IAAI,CAW1D;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAI,MAAM,CASzF;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAI,MAAM,CAMjE"}
|
|
@@ -77,6 +77,7 @@ const COLOR_MODE_COOKIE = "vc-color-mode";
|
|
|
77
77
|
event.response.headers.set("content-security-policy", "frame-ancestors 'none'");
|
|
78
78
|
event.response.headers.set("x-frame-options", "DENY");
|
|
79
79
|
event.response.headers.set("referrer-policy", "no-referrer");
|
|
80
|
+
event.response.headers.set("cache-control", "no-store");
|
|
80
81
|
}
|
|
81
82
|
/**
|
|
82
83
|
* Rewrite root-absolute asset references (script/link/preload tags emitted
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html.mjs","names":["useRequestCookie","LOCALE_COOKIE","COLOR_MODE_COOKIE","readUIClientPreferences","event","locale","colorMode","stampHtmlAttributes","html","preferences","htmlAttrs","test","replace","replaceTemplateMarker","marker","value","injectHeadContent","content","index","toLowerCase","indexOf","slice","stampDocumentTitle","title","applyUIPageHeaders","response","headers","set","rebaseAssetURLs","basePath","viteBase","RegExp","_match","attribute","serializeInlineScriptJSON","JSON","stringify"],"sources":["../../../../../src/adapters/http/ui/shared/html.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 { useRequestCookie } from '@routup/basic/cookie';\nimport type { IAppEvent } from 'routup';\nimport { LOCALE_COOKIE } from '../../request/helpers/locale.ts';\n\nconst COLOR_MODE_COOKIE = 'vc-color-mode';\n\nexport type UIClientPreferences = {\n locale?: string,\n colorMode?: string,\n};\n\n/**\n * Mirror @vuecs/nuxt's SSR plugins: resolve the shared cookies server-side\n * so the HTML shell already carries the `.dark`/`.light` class and lang\n * attribute (no flash) and the client app hydrates from the same values.\n * The cookies are shared between every console on the origin.\n */\nexport function readUIClientPreferences(event: IAppEvent) : UIClientPreferences {\n return {\n locale: useRequestCookie(event, LOCALE_COOKIE),\n colorMode: useRequestCookie(event, COLOR_MODE_COOKIE),\n };\n}\n\n/**\n * Stamp the resolved preferences onto the opening `<html>` tag. Matched by\n * pattern rather than an exact literal, so a reformatted tag / dev-mode\n * transformIndexHtml rewrite still gets the lang + color-mode attributes\n * (no silent FOUC).\n */\nexport function stampHtmlAttributes(html: string, preferences: UIClientPreferences) : string {\n let htmlAttrs = 'lang=\"en\"';\n if (preferences.locale && /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]+)*$/.test(preferences.locale)) {\n htmlAttrs = `lang=\"${preferences.locale}\"`;\n }\n if (preferences.colorMode === 'dark' || preferences.colorMode === 'light') {\n htmlAttrs += ` class=\"${preferences.colorMode}\"`;\n }\n\n return html.replace(/<html\\b[^>]*>/i, () => `<html ${htmlAttrs}>`);\n}\n\n/**\n * Replace a template marker (`<!--app-html-->`, `<!--account-config-->`, ...)\n * with a rendered value.\n *\n * The replacement is passed as a FUNCTION on purpose. A string replacement\n * expands the `$&`, `` $` ``, `$'` and `$$` patterns, so a `$'` anywhere in\n * the value splices the template's own text back into the page. The values\n * spliced here carry raw request input (the SSR hydration payload reflects\n * query parameters), which made `?token=$'` return a page whose inline\n * payload script was no longer valid JavaScript. Escaping cannot prevent\n * this: the expansion happens after the value was built.\n */\nexport function replaceTemplateMarker(html: string, marker: string, value: string) : string {\n return html.replace(marker, () => value);\n}\n\n/**\n * Insert markup immediately before `</head>`.\n *\n * Deliberately not a template marker: injecting at the closing tag works\n * against console bundles that were built BEFORE this feature existed, so\n * server-core never silently loses theming when it runs against an older\n * console package. A shell without a `</head>` is returned unchanged.\n *\n * Function-replacement form for the same reason as replaceTemplateMarker.\n */\nexport function injectHeadContent(html: string, content: string) : string {\n if (!content) {\n return html;\n }\n\n const index = html.toLowerCase().indexOf('</head>');\n if (index === -1) {\n return html;\n }\n\n return html.slice(0, index) + content + html.slice(index);\n}\n\n/**\n * Replace the document title. The value must already be HTML-escaped by\n * the caller; matched by pattern so a reformatted tag still hits.\n */\nexport function stampDocumentTitle(html: string, title: string) : string {\n return html.replace(/<title\\b[^>]*>[\\s\\S]*?<\\/title>/i, () => `<title>${title}</title>`);\n}\n\n/**\n * Response headers every served console page shares.\n *\n * Clickjacking guard: the pages mutate state behind explicit clicks and\n * hydrate first-party session state, so framing them would make\n * click-gating defeatable via overlay attacks — deny embedding entirely\n * (iframe-based silent renew is therefore unsupported). The URLs routinely\n * carry sensitive query params (id_token_hint, code, redirect, state) —\n * never leak them via Referer.\n */\nexport function applyUIPageHeaders(event: IAppEvent) : void {\n event.response.headers.set('content-type', 'text/html; charset=utf-8');\n // The shell is stamped from the vc-locale / vc-color-mode cookies, so\n // it genuinely varies by them. It carried no Vary header at all.\n event.response.headers.set('vary', 'cookie');\n event.response.headers.set('content-security-policy', \"frame-ancestors 'none'\");\n event.response.headers.set('x-frame-options', 'DENY');\n event.response.headers.set('referrer-policy', 'no-referrer');\n}\n\n/**\n * Rewrite root-absolute asset references (script/link/preload tags emitted\n * with a fixed vite base, e.g. /public/ or /account/) so they carry the\n * path prefix under which authup is publicly served (e.g. /auth/public/...).\n * The reverse proxy is expected to strip the prefix before the request\n * reaches authup, so the assets middleware keeps serving at the fixed base.\n */\nexport function rebaseAssetURLs(html: string, basePath: string, viteBase: string) : string {\n if (!basePath) {\n return html;\n }\n\n return html.replace(\n new RegExp(`(src|href)=\"${viteBase.replace(/[.*+?^${}()|[\\]\\\\/]/g, '\\\\$&')}`, 'g'),\n (_match, attribute) => `${attribute}=\"${basePath}${viteBase}`,\n );\n}\n\n/**\n * Escape characters that would otherwise let a value break out of an inline\n * `<script>` context (`</script>`) or terminate the JS string literal via\n * the U+2028/U+2029 line separators. Every window-global config/payload a\n * console page inlines must pass through this — never rely on the VALUES\n * staying benign.\n */\nexport function serializeInlineScriptJSON(value: unknown) : string {\n return JSON.stringify(value)\n .replace(/</g, '\\\\u003c')\n .replace(/>/g, '\\\\u003e')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029');\n}\n"],"mappings":";;;;;;AAWA,MAAME,oBAAoB;;;;;;GAa1B,SAAgBC,wBAAwBC,OAAgB;CACpD,OAAO;EACHC,QAAQL,iBAAiBI,OAAOH,aAAAA;EAChCK,WAAWN,iBAAiBI,OAAOF,iBAAAA;CACvC;AACJ;;;;;;GAQA,SAAgBK,oBAAoBC,MAAcC,aAAgC;CAC9E,IAAIC,YAAY;CAChB,IAAID,YAAYJ,UAAU,kCAAkCM,KAAKF,YAAYJ,MAAM,GAC/EK,YAAY,SAASD,YAAYJ,OAAO;CAE5C,IAAII,YAAYH,cAAc,UAAUG,YAAYH,cAAc,SAC9DI,aAAa,WAAWD,YAAYH,UAAU;CAGlD,OAAOE,KAAKI,QAAQ,wBAAwB,SAASF,UAAU,EAAE;AACrE;;;;;;;;;;;;GAcA,SAAgBG,sBAAsBL,MAAcM,QAAgBC,OAAa;CAC7E,OAAOP,KAAKI,QAAQE,cAAcC,KAAAA;AACtC;;;;;;;;;;GAYA,SAAgBC,kBAAkBR,MAAcS,SAAe;CAC3D,IAAI,CAACA,SACD,OAAOT;CAGX,MAAMU,QAAQV,KAAKW,YAAW,CAAA,CAAGC,QAAQ,SAAA;CACzC,IAAIF,UAAU,IACV,OAAOV;CAGX,OAAOA,KAAKa,MAAM,GAAGH,KAAAA,IAASD,UAAUT,KAAKa,MAAMH,KAAAA;AACvD;;;;GAMA,SAAgBI,mBAAmBd,MAAce,OAAa;CAC1D,OAAOf,KAAKI,QAAQ,0CAA0C,UAAUW,MAAM,SAAS;AAC3F;;;;;;;;;;GAYA,SAAgBC,mBAAmBpB,OAAgB;CAC/CA,MAAMqB,SAASC,QAAQC,IAAI,gBAAgB,0BAAA;CAG3CvB,MAAMqB,SAASC,QAAQC,IAAI,QAAQ,QAAA;CACnCvB,MAAMqB,SAASC,QAAQC,IAAI,2BAA2B,wBAAA;CACtDvB,MAAMqB,SAASC,QAAQC,IAAI,mBAAmB,MAAA;CAC9CvB,MAAMqB,SAASC,QAAQC,IAAI,mBAAmB,aAAA;
|
|
1
|
+
{"version":3,"file":"html.mjs","names":["useRequestCookie","LOCALE_COOKIE","COLOR_MODE_COOKIE","readUIClientPreferences","event","locale","colorMode","stampHtmlAttributes","html","preferences","htmlAttrs","test","replace","replaceTemplateMarker","marker","value","injectHeadContent","content","index","toLowerCase","indexOf","slice","stampDocumentTitle","title","applyUIPageHeaders","response","headers","set","rebaseAssetURLs","basePath","viteBase","RegExp","_match","attribute","serializeInlineScriptJSON","JSON","stringify"],"sources":["../../../../../src/adapters/http/ui/shared/html.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 { useRequestCookie } from '@routup/basic/cookie';\nimport type { IAppEvent } from 'routup';\nimport { LOCALE_COOKIE } from '../../request/helpers/locale.ts';\n\nconst COLOR_MODE_COOKIE = 'vc-color-mode';\n\nexport type UIClientPreferences = {\n locale?: string,\n colorMode?: string,\n};\n\n/**\n * Mirror @vuecs/nuxt's SSR plugins: resolve the shared cookies server-side\n * so the HTML shell already carries the `.dark`/`.light` class and lang\n * attribute (no flash) and the client app hydrates from the same values.\n * The cookies are shared between every console on the origin.\n */\nexport function readUIClientPreferences(event: IAppEvent) : UIClientPreferences {\n return {\n locale: useRequestCookie(event, LOCALE_COOKIE),\n colorMode: useRequestCookie(event, COLOR_MODE_COOKIE),\n };\n}\n\n/**\n * Stamp the resolved preferences onto the opening `<html>` tag. Matched by\n * pattern rather than an exact literal, so a reformatted tag / dev-mode\n * transformIndexHtml rewrite still gets the lang + color-mode attributes\n * (no silent FOUC).\n */\nexport function stampHtmlAttributes(html: string, preferences: UIClientPreferences) : string {\n let htmlAttrs = 'lang=\"en\"';\n if (preferences.locale && /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]+)*$/.test(preferences.locale)) {\n htmlAttrs = `lang=\"${preferences.locale}\"`;\n }\n if (preferences.colorMode === 'dark' || preferences.colorMode === 'light') {\n htmlAttrs += ` class=\"${preferences.colorMode}\"`;\n }\n\n return html.replace(/<html\\b[^>]*>/i, () => `<html ${htmlAttrs}>`);\n}\n\n/**\n * Replace a template marker (`<!--app-html-->`, `<!--account-config-->`, ...)\n * with a rendered value.\n *\n * The replacement is passed as a FUNCTION on purpose. A string replacement\n * expands the `$&`, `` $` ``, `$'` and `$$` patterns, so a `$'` anywhere in\n * the value splices the template's own text back into the page. The values\n * spliced here carry raw request input (the SSR hydration payload reflects\n * query parameters), which made `?token=$'` return a page whose inline\n * payload script was no longer valid JavaScript. Escaping cannot prevent\n * this: the expansion happens after the value was built.\n */\nexport function replaceTemplateMarker(html: string, marker: string, value: string) : string {\n return html.replace(marker, () => value);\n}\n\n/**\n * Insert markup immediately before `</head>`.\n *\n * Deliberately not a template marker: injecting at the closing tag works\n * against console bundles that were built BEFORE this feature existed, so\n * server-core never silently loses theming when it runs against an older\n * console package. A shell without a `</head>` is returned unchanged.\n *\n * Function-replacement form for the same reason as replaceTemplateMarker.\n */\nexport function injectHeadContent(html: string, content: string) : string {\n if (!content) {\n return html;\n }\n\n const index = html.toLowerCase().indexOf('</head>');\n if (index === -1) {\n return html;\n }\n\n return html.slice(0, index) + content + html.slice(index);\n}\n\n/**\n * Replace the document title. The value must already be HTML-escaped by\n * the caller; matched by pattern so a reformatted tag still hits.\n */\nexport function stampDocumentTitle(html: string, title: string) : string {\n return html.replace(/<title\\b[^>]*>[\\s\\S]*?<\\/title>/i, () => `<title>${title}</title>`);\n}\n\n/**\n * Response headers every served console page shares.\n *\n * Clickjacking guard: the pages mutate state behind explicit clicks and\n * hydrate first-party session state, so framing them would make\n * click-gating defeatable via overlay attacks — deny embedding entirely\n * (iframe-based silent renew is therefore unsupported). The URLs routinely\n * carry sensitive query params (id_token_hint, code, redirect, state) —\n * never leak them via Referer.\n */\nexport function applyUIPageHeaders(event: IAppEvent) : void {\n event.response.headers.set('content-type', 'text/html; charset=utf-8');\n // The shell is stamped from the vc-locale / vc-color-mode cookies, so\n // it genuinely varies by them. It carried no Vary header at all.\n event.response.headers.set('vary', 'cookie');\n event.response.headers.set('content-security-policy', \"frame-ancestors 'none'\");\n event.response.headers.set('x-frame-options', 'DENY');\n event.response.headers.set('referrer-policy', 'no-referrer');\n // Login pages, and the federated callback's interstitial carries a\n // fresh authorization code in its payload.\n event.response.headers.set('cache-control', 'no-store');\n}\n\n/**\n * Rewrite root-absolute asset references (script/link/preload tags emitted\n * with a fixed vite base, e.g. /public/ or /account/) so they carry the\n * path prefix under which authup is publicly served (e.g. /auth/public/...).\n * The reverse proxy is expected to strip the prefix before the request\n * reaches authup, so the assets middleware keeps serving at the fixed base.\n */\nexport function rebaseAssetURLs(html: string, basePath: string, viteBase: string) : string {\n if (!basePath) {\n return html;\n }\n\n return html.replace(\n new RegExp(`(src|href)=\"${viteBase.replace(/[.*+?^${}()|[\\]\\\\/]/g, '\\\\$&')}`, 'g'),\n (_match, attribute) => `${attribute}=\"${basePath}${viteBase}`,\n );\n}\n\n/**\n * Escape characters that would otherwise let a value break out of an inline\n * `<script>` context (`</script>`) or terminate the JS string literal via\n * the U+2028/U+2029 line separators. Every window-global config/payload a\n * console page inlines must pass through this — never rely on the VALUES\n * staying benign.\n */\nexport function serializeInlineScriptJSON(value: unknown) : string {\n return JSON.stringify(value)\n .replace(/</g, '\\\\u003c')\n .replace(/>/g, '\\\\u003e')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029');\n}\n"],"mappings":";;;;;;AAWA,MAAME,oBAAoB;;;;;;GAa1B,SAAgBC,wBAAwBC,OAAgB;CACpD,OAAO;EACHC,QAAQL,iBAAiBI,OAAOH,aAAAA;EAChCK,WAAWN,iBAAiBI,OAAOF,iBAAAA;CACvC;AACJ;;;;;;GAQA,SAAgBK,oBAAoBC,MAAcC,aAAgC;CAC9E,IAAIC,YAAY;CAChB,IAAID,YAAYJ,UAAU,kCAAkCM,KAAKF,YAAYJ,MAAM,GAC/EK,YAAY,SAASD,YAAYJ,OAAO;CAE5C,IAAII,YAAYH,cAAc,UAAUG,YAAYH,cAAc,SAC9DI,aAAa,WAAWD,YAAYH,UAAU;CAGlD,OAAOE,KAAKI,QAAQ,wBAAwB,SAASF,UAAU,EAAE;AACrE;;;;;;;;;;;;GAcA,SAAgBG,sBAAsBL,MAAcM,QAAgBC,OAAa;CAC7E,OAAOP,KAAKI,QAAQE,cAAcC,KAAAA;AACtC;;;;;;;;;;GAYA,SAAgBC,kBAAkBR,MAAcS,SAAe;CAC3D,IAAI,CAACA,SACD,OAAOT;CAGX,MAAMU,QAAQV,KAAKW,YAAW,CAAA,CAAGC,QAAQ,SAAA;CACzC,IAAIF,UAAU,IACV,OAAOV;CAGX,OAAOA,KAAKa,MAAM,GAAGH,KAAAA,IAASD,UAAUT,KAAKa,MAAMH,KAAAA;AACvD;;;;GAMA,SAAgBI,mBAAmBd,MAAce,OAAa;CAC1D,OAAOf,KAAKI,QAAQ,0CAA0C,UAAUW,MAAM,SAAS;AAC3F;;;;;;;;;;GAYA,SAAgBC,mBAAmBpB,OAAgB;CAC/CA,MAAMqB,SAASC,QAAQC,IAAI,gBAAgB,0BAAA;CAG3CvB,MAAMqB,SAASC,QAAQC,IAAI,QAAQ,QAAA;CACnCvB,MAAMqB,SAASC,QAAQC,IAAI,2BAA2B,wBAAA;CACtDvB,MAAMqB,SAASC,QAAQC,IAAI,mBAAmB,MAAA;CAC9CvB,MAAMqB,SAASC,QAAQC,IAAI,mBAAmB,aAAA;CAG9CvB,MAAMqB,SAASC,QAAQC,IAAI,iBAAiB,UAAA;AAChD;;;;;;;GASA,SAAgBC,gBAAgBpB,MAAcqB,UAAkBC,UAAgB;CAC5E,IAAI,CAACD,UACD,OAAOrB;CAGX,OAAOA,KAAKI,QACR,IAAImB,OAAO,eAAeD,SAASlB,QAAQ,wBAAwB,MAAA,KAAW,GAAA,IAC7EoB,QAAQC,cAAc,GAAGA,UAAU,IAAIJ,WAAWC,UAAU;AAErE;;;;;;;GASA,SAAgBI,0BAA0BnB,OAAc;CACpD,OAAOoB,KAAKC,UAAUrB,KAAAA,CAAAA,CACjBH,QAAQ,MAAM,SAAA,CAAA,CACdA,QAAQ,MAAM,SAAA,CAAA,CACdA,QAAQ,WAAW,SAAA,CAAA,CACnBA,QAAQ,WAAW,SAAA;AAC5B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../../../../src/app/modules/config/normalize.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEtD,wBAAsB,eAAe,CAAC,KAAK,GAAE,WAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../../../../src/app/modules/config/normalize.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEtD,wBAAsB,eAAe,CAAC,KAAK,GAAE,WAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CA4L9E"}
|
|
@@ -16,7 +16,6 @@ import process from "node:process";
|
|
|
16
16
|
//#region src/app/modules/config/normalize.ts
|
|
17
17
|
async function normalizeConfig(input = {}) {
|
|
18
18
|
const parsed = await parseConfig(input);
|
|
19
|
-
const writableDirectoryPath = parsed.writableDirectoryPath || path.join(process.cwd(), "writable");
|
|
20
19
|
const port = parsed.port ?? 3001;
|
|
21
20
|
let host = parsed.host || "0.0.0.0";
|
|
22
21
|
let publicUrl;
|
|
@@ -38,7 +37,7 @@ async function normalizeConfig(input = {}) {
|
|
|
38
37
|
const config = {
|
|
39
38
|
env,
|
|
40
39
|
rootPath: process.cwd(),
|
|
41
|
-
writableDirectoryPath,
|
|
40
|
+
writableDirectoryPath: "writable",
|
|
42
41
|
themeDirectoryPath: "",
|
|
43
42
|
themeFragmentsEnabled: false,
|
|
44
43
|
authConsolePath: "",
|
|
@@ -94,6 +93,7 @@ async function normalizeConfig(input = {}) {
|
|
|
94
93
|
...parsed,
|
|
95
94
|
trustedOrigins
|
|
96
95
|
};
|
|
96
|
+
config.writableDirectoryPath = path.resolve(config.rootPath, config.writableDirectoryPath || "writable");
|
|
97
97
|
if (config.themeDirectoryPath) config.themeDirectoryPath = path.resolve(config.rootPath, config.themeDirectoryPath);
|
|
98
98
|
if (config.authConsolePath) config.authConsolePath = path.resolve(config.rootPath, config.authConsolePath);
|
|
99
99
|
if (config.accountConsolePath) config.accountConsolePath = path.resolve(config.rootPath, config.accountConsolePath);
|