# @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.

```ts
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:

```ts
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:

```env
AUTH_SERVICE_MAGIC_LINK_VERIFY_URL=/verify
```

For 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:

```ts
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.

- `redirectUri` is **not** configurable; the library derives it from `appUrl`
  as `${appUrl}/auth/google/callback` and `authRoutes` serves that path with
  `AuthGoogleCallbackPage`. Register exactly this URI in the Google Cloud
  Console.
- An empty `clientId` is 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 `state` in sessionStorage
  (`auth-google-state`) and performs a full-page redirect to
  `https://accounts.google.com/o/oauth2/v2/auth` with
  `client_id`, `redirect_uri`, `response_type=code`,
  `scope=openid email profile`, and `state`.
- 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:

```ts
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:

1. create a 256-bit PKCE verifier and S256 challenge;
2. `POST {gateway.baseUrl}{startEndpoint}` with `{ mfeId, returnUrl,
codeChallenge, codeChallengeMethod: 'S256' }`;
3. accept only an authorization URL on the exact trusted Google
   `https://accounts.google.com/o/oauth2/v2/auth` endpoint, then redirect;
4. receive only `handoffCode` at the MFE callback, scrub all callback query
   parameters, and consume the verifier once;
5. `POST {gateway.baseUrl}{handoffEndpoint}` with credentials and
   `{ mfeId, returnUrl, handoffCode, codeVerifier }`;
6. 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:

```http
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:

1. exchanges `code` at Google's token endpoint using the server-side client
   secret and the received `redirectUri`;
2. sets the refresh-token cookie, exactly like magic-link verify does;
3. 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.

```ts
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:

```ts
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:

```bash
bun run build:library
```

That command builds the UI package, then the SDK, and finally packages this library:

```bash
bun run build:ui
bun run gen:sdk
bunx ng build sdk
bunx ng build library
```

The packaged output is written to `dist/library`, including the reusable `@edsis/angular/module/auth` entry point.

## Full release

Run the guarded release flow with:

```bash
bun run release:library
```

The 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`, or `Z`
- map `X` to major, `Y` to minor, and `Z` to 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/library` to the public npm registry

For a non-interactive release, provide the bump and confirmation flags explicitly:

```bash
bun run release:library --bump Z --yes
```

Preview the release plan without changing files:

```bash
bun run release:library:dry-run --bump Y --yes
```

## Versioning

Update the publishable package version in `library/angular/package.json` with:

```bash
bun run version:library patch
bun run version:library minor
bun run version:library 1.2.3
bun run version:library prerelease --preid beta
```

The 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:

```bash
bun run publish
```

The script will:

- verify the active npm login against the public npm registry
- ask for a version bump using `X`, `Y`, or `Z`
- update `library/sdk/package.json`, `config/sdk.config.json`, and `library/angular/package.json`
- run `bun run build:library`, which builds the UI package, then the SDK, and finally the library
- publish `dist/sdk` and then `dist/library` to npm public with `--access public`

For a non-interactive publish, provide the bump and confirmation flags explicitly:

```bash
bun run publish --bump Z --yes
```

Preview the publish plan without changing files:

```bash
bun run publish --dry-run --bump Y --yes
```

Publish only the SDK package:

```bash
bun run publish --sdk --bump Z --yes
```

Publish only the library package:

```bash
bun run publish --lib --bump Z --yes
```

Preview the npm tarball:

```bash
bun run pack:library
```

Publish to npm:

```bash
bun run publish
bun run publish --tag next
bun run publish:sdk
bun run publish:library
```

Run a local publish dry-run:

```bash
bun run publish:sdk:dry-run
bun run publish:library:dry-run
```

When 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:

```bash
bun run update:edsis
```

If a specific version needs to be tested:

```bash
bun run update:edsis 21.1.11
```

`publish` 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:

```bash
bun run publish --provenance
```

## GitHub publish workflow

The repository includes `.github/workflows/publish-library.yml`.

It supports:

- pushing a tag named `library-vX.Y.Z`
- manual `workflow_dispatch` runs 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:

1. Run `bun run release:library`.
2. Select `X`, `Y`, or `Z`, then confirm the computed version.
3. Let the script build, commit, push, and publish the package.
