@edsis/angular
v22.1.6
Published
Readme
@edsis/angular
Angular library package for Edsis shared application features.
Published entry points:
@edsis/angular@edsis/angular/services/runtime@edsis/angular/module/auth@edsis/angular/platform/mfe
The generated API SDK is published separately as @edsis/sdk.
module/auth: persistent cookie session
Configure the module once at application bootstrap. provideEdsisAuth() runs
AuthService.initialize() through provideAppInitializer, before initial
navigation and route guards. It restores a session with the HttpOnly refresh
cookie, while the access token is kept only in the running application's
memory.
provideEdsisAuth({
apiUrl: environment.apiUrl,
appUrl: environment.appUrl,
});Use AuthService for password login/logout and its user, accessToken, and
isAuthenticated signals for UI state. The library calls the configured
/v1/auth/* endpoints with credentials.
The interceptor adds withCredentials and the in-memory bearer token to API
requests, coalesces concurrent 401 refreshes, retries once, and redirects to
the configured login route when refresh fails. It never writes authentication
tokens to sessionStorage, localStorage, or document.cookie.
For optional native notification badges, call
setAuthNotificationBadge(unreadCount); it is a no-op in a normal browser
and uses the bundled Tauri API when a native host is present.
The persistent cookie belongs to the browser/WebView origin. Therefore a production, development, and local build must use distinct origins; keeping an origin and its WebView data directory stable preserves the session across a Tauri restart or update.
services/runtime: desktop, tablet, and browser context
Register provideEdsisRuntime() before rendering the structural shell. It
validates a schema V2 window.EDSIS_RUNTIME, falls back to the runtime_context
Tauri command and edsis:runtime-ready event, then safely resolves a responsive
web context in an ordinary browser. Trusted schema V1 payloads are accepted and
normalized to V2 during migration. V2 adds the canonical deviceType value:
browser, windows, macos, android, or ipados.
RuntimeContextService exposes deviceType plus signals for desktop, iPadOS,
Android tablet, and the shell capability list without using the User-Agent. It
also writes the resolved host/platform/device/device-type/profile to
data-edsis-* attributes on the document root. browser is reserved for a
non-Tauri host; an invalid native response fails initialization instead of being
silently mislabeled as a browser.
When the consumer also uses provideUiTheme(), register the opt-in bridge so UI
components and CSS receive the same resolved device:
import { provideRuntimeThemeDevice } from '@edsis/angular/block/themes';
import { provideEdsisRuntime } from '@edsis/angular/services/runtime';
import { provideUiTheme } from '@edsis/theme/styles';
providers: [provideUiTheme(), provideEdsisRuntime(), provideRuntimeThemeDevice()];The bridge waits for runtime initialization and then updates
ThemeDeviceService once. This keeps the base runtime entry point independent
from the optional theme package.
TabletViewportService reports landscape and the 600 CSS pixel minimum. The
consumer decides where to render its blocking view and must gate it with
RuntimeContextService.isTabletShell() so narrow browser windows are not
blocked.
module/auth: Magic-link destination
When the login form is submitted, the auth module sends the origin derived
from appUrl as frontend_url. This lets the backend keep only the route path
in its environment configuration:
AUTH_SERVICE_MAGIC_LINK_VERIFY_URL=/verifyFor example, an app configured with appUrl: 'https://edsis.dev' submits
frontend_url: 'https://edsis.dev'; the backend creates the final destination
https://edsis.dev/verify#token=.... An absolute backend verify URL remains a
supported fallback for older deployments.
module/auth: Google Sign-In
@edsis/angular/module/auth ships two discriminated Google Sign-In modes. The
existing direct authorization-code mode remains backward compatible and stays
disabled until a client ID is configured:
provideEdsisAuth({
apiUrl: environment.apiUrl,
appUrl: environment.appUrl,
oauth: {
google: { clientId: environment.google.clientId },
nativeGoogle: { apiUrl: '/api' },
},
});On desktop/browser the existing authorization-code redirect remains
unchanged. A tablet runtime advertising google-native-sign-in instead asks
the same-origin challenge endpoint, invokes the native Google adapter, and
posts its short-lived ID token to /api/v1/auth/google/native. Refresh, user,
and logout requests then stay on /api so the host-only HttpOnly cookie is
first-party. A tablet never falls back to the browser OAuth redirect: when the
native capability/plugin is unavailable, Google Sign-In is disabled with an
explicit error. Access, refresh, and Google ID tokens are never persisted by
JavaScript.
redirectUriis not configurable; the library derives it fromappUrlas${appUrl}/auth/google/callbackandauthRoutesserves that path withAuthGoogleCallbackPage. Register exactly this URI in the Google Cloud Console.- An empty
clientIdis treated as "not configured", so a build-time injected environment value can be passed through as-is. - Clicking the Google button stores a crypto-random
statein sessionStorage (auth-google-state) and performs a full-page redirect tohttps://accounts.google.com/o/oauth2/v2/authwithclient_id,redirect_uri,response_type=code,scope=openid email profile, andstate. - Microsoft and Apple remain placeholders.
Central Auth Gateway mode for MFEs
An MFE does not need its own Google client configuration. Select gateway
mode with the lower-kebab mfeId (maximum 64 characters) registered in the
central Gateway allowlist:
provideEdsisAuth({
apiUrl: environment.apiUrl,
appUrl: environment.appUrl,
oauth: {
google: {
mode: 'gateway',
mfeId: 'developer-guide',
// Optional. Omit `gateway` to use `apiUrl` and the default endpoints.
gateway: {
baseUrl: `${environment.apiUrl}/auth-gateway`,
// Optional; these are the defaults:
startEndpoint: '/v1/auth/google/start',
handoffEndpoint: '/v1/auth/google/handoff',
},
},
},
});The library derives the exact MFE returnUrl from appUrl as
${appUrl}/auth/google/callback. Register that pair with mfeId on the
Gateway. Only the Gateway callback is registered with Google. gateway and
gateway.baseUrl are optional and default to apiUrl. An explicit baseUrl
may use a different path, but its origin (scheme, host, and port) must exactly
match apiUrl; configuration fails immediately otherwise. This keeps the
handoff, refresh, current-user, and logout requests on one cookie origin.
baseUrl must use HTTPS; plain HTTP is accepted only for exact loopback hosts
(localhost, 127.0.0.1, and [::1]) during local development. The browser
supplies the trusted Origin; the Gateway requires it to equal the origin of
returnUrl.
The application origin must also be schemeful same-site with this cookie
origin, or route auth through an application-owned same-origin BFF. CORS plus
SameSite=None is not sufficient on browsers that block third-party cookies;
cross-site deployments must not be promoted until login, reload/refresh, and
logout pass with third-party cookies blocked.
The browser flow is:
- create a 256-bit PKCE verifier and S256 challenge;
POST {gateway.baseUrl}{startEndpoint}with{ mfeId, returnUrl, codeChallenge, codeChallengeMethod: 'S256' };- accept only an authorization URL on the exact trusted Google
https://accounts.google.com/o/oauth2/v2/authendpoint, then redirect; - receive only
handoffCodeat the MFE callback, scrub all callback query parameters, and consume the verifier once; POST {gateway.baseUrl}{handoffEndpoint}with credentials and{ mfeId, returnUrl, handoffCode, codeVerifier };- pass the session response to
AuthService.acceptTokenResponse()and keep its access token in memory. Refresh, current-user, and logout then use the central Gateway's HttpOnly refresh cookie.
sessionStorage contains only auth-google-gateway-session: the PKCE verifier,
mfeId, returnUrl, and expiry metadata. It never contains an access token,
refresh token, Google token, authorization URL, or handoff code. The record is
removed before every success/error handoff branch so reload and replay fail
closed. A failed start or handoff returns the UI to a retryable error state.
Backend contract
The code exchange happens on the backend so the client secret never reaches
the frontend. The callback page calls the generated SDK method
AuthSessionService.exchangeGoogleCode(...) from @edsis/sdk/auth (no raw
HttpClient, consistent with verifyMagicLink/refreshAuthToken), which maps
to:
POST {apiUrl}/v1/auth/google
Body: { "code": string, "redirectUri": string }Because the path lives under /v1/auth/, edsisAuthInterceptor sends it with
cookie credentials automatically. The endpoint:
- exchanges
codeat Google's token endpoint using the server-side client secret and the receivedredirectUri; - sets the refresh-token cookie, exactly like magic-link verify does;
- responds with a session payload identical to
POST /v1/auth/verify({ status, message, data: { access_token, user_id, role, name } }).
On success the callback page passes the response to AuthService, which keeps
the access token only in memory, then redirects to authenticatedHomePath; on
failure it shows the backend message when one is provided (with a retry link
back to loginPath).
The endpoint definition comes from openapi.bundle.yaml; regenerate the SDK in
library/sdk after spec changes with bun run gen:sdk (driven by
config/sdk.config.json, generator: @ojiepermana/angular-sdk).
platform/mfe: standalone profile and hybrid contract
Every MFE registry entry declares standaloneProfile: 'mfe-standalone'; the
remote contract does not duplicate that registry-owned value. The named profile
centralizes the current Developer standalone defaults: desktop/compact theme,
base/horizontal/full layout, and horizontal/flyout/top navigation. Hosted
routes/components inherit the shell singletons and must not register standalone
providers again.
import { defineMfe, provideStandaloneMfe, readMfeContract } from '@edsis/angular/platform/mfe';
export const mfe = defineMfe({
contractVersion: 1,
id: 'developer-guide',
displayName: 'Developer Guide',
featureRoutes,
navigation,
capabilities,
exposures,
});
bootstrapApplication(App, {
providers: [
provideStandaloneMfe({
standaloneProfile: registration.standaloneProfile,
auth: environment.auth,
runtime: environment.runtime,
}),
],
});provideStandaloneMfe() owns runtime, theme, profile preferences, navigation
defaults, and auth bootstrap for standalone mode. Shared Edsis components own
their accessibility behavior; no parallel per-MFE a11y singleton is required.
Federation hosts load the remote namespace through the public boundary helper:
const contract = readMfeContract(remoteModule, {
id: registration.id,
supportedContractVersions: registration.supportedContractVersions,
});The helper requires the exact named mfe export, validates the complete V1
shape first, then checks registry ID and supported contract majors. Shape
failures throw MfeContractValidationError with diagnostic issues; namespace
and registry-expectation failures use the same error class with an empty issue
array and a boundary-specific message.
Generate and build
Use the root workspace scripts so SDK generation and library packaging stay in sync:
bun run build:libraryThat command builds the UI package, then the SDK, and finally packages this library:
bun run build:ui
bun run gen:sdk
bunx ng build sdk
bunx ng build libraryThe packaged output is written to dist/library, including the reusable @edsis/angular/module/auth entry point.
Full release
Run the guarded release flow with:
bun run release:libraryThe script will:
- reject the release if the git worktree is dirty
- reject the release if the current branch still has commits that are not pushed, is behind, or is diverged from its upstream branch
- ask for a version bump using
X,Y, orZ - map
Xto major,Yto minor, andZto patch, then increment the current version automatically - ask for a final confirmation before continuing
- run
bun run build:library - commit the version bump and generated SDK updates with
update version ke {{version}} - push the commit
- publish
dist/libraryto the public npm registry
For a non-interactive release, provide the bump and confirmation flags explicitly:
bun run release:library --bump Z --yesPreview the release plan without changing files:
bun run release:library:dry-run --bump Y --yesVersioning
Update the publishable package version in library/angular/package.json with:
bun run version:library patch
bun run version:library minor
bun run version:library 1.2.3
bun run version:library prerelease --preid betaThe version script only updates the source package manifest. The built package in
dist/library picks up the version on the next bun run build:library.
Pack and publish locally
Run the interactive local publish workflow with:
bun run publishThe script will:
- verify the active npm login against the public npm registry
- ask for a version bump using
X,Y, orZ - update
library/sdk/package.json,config/sdk.config.json, andlibrary/angular/package.json - run
bun run build:library, which builds the UI package, then the SDK, and finally the library - publish
dist/sdkand thendist/libraryto npm public with--access public
For a non-interactive publish, provide the bump and confirmation flags explicitly:
bun run publish --bump Z --yesPreview the publish plan without changing files:
bun run publish --dry-run --bump Y --yesPublish only the SDK package:
bun run publish --sdk --bump Z --yesPublish only the library package:
bun run publish --lib --bump Z --yesPreview the npm tarball:
bun run pack:libraryPublish to npm:
bun run publish
bun run publish --tag next
bun run publish:sdk
bun run publish:libraryRun a local publish dry-run:
bun run publish:sdk:dry-run
bun run publish:library:dry-runWhen updating @ojiepermana/angular, use the helper below instead of a plain bun update so the version pin, patch mapping, install step, and SDK build verification stay in sync:
bun run update:edsisIf a specific version needs to be tested:
bun run update:edsis 21.1.11publish publishes @edsis/sdk before @edsis/angular. publish:sdk and publish:library use the same helper with --sdk or --lib and force --access public.
For local publishes, the script does not add --provenance by default because npm provenance generation is not supported outside compatible CI providers. If you need provenance in a supported CI environment, pass it explicitly:
bun run publish --provenanceGitHub publish workflow
The repository includes .github/workflows/publish-library.yml.
It supports:
- pushing a tag named
library-vX.Y.Z - manual
workflow_dispatchruns with an npm tag and optional dry-run
The workflow uses npm trusted publishing via GitHub Actions OIDC, so it does
not require a repository NPM_TOKEN secret. The npm package must be configured
to trust this repository workflow as a publisher. On tag pushes, the tag
version must match library/angular/package.json.
Recommended local release flow:
- Run
bun run release:library. - Select
X,Y, orZ, then confirm the computed version. - Let the script build, commit, push, and publish the package.
