@kaidn/angular
v2.0.0
Published
Angular bindings for Kaidn device intelligence. A DI service over @kaidn/fp, built from runtime API only so no Angular compiler is involved.
Maintainers
Readme
@kaidn/angular
Angular bindings for Kaidn device intelligence. A DI service over
@kaidn/fp, built from runtime API only so no
Angular compiler is involved.
npm install @kaidn/angular// app.config.ts
import { provideKaidn } from "@kaidn/angular";
bootstrapApplication(AppComponent, {
providers: [provideKaidn({ publishableKey: "pk_live_…" })],
});import { Component, inject } from "@angular/core";
import { KaidnService } from "@kaidn/angular";
@Component({ /* … */ })
export class SignupComponent {
private readonly kaidn = inject(KaidnService);
async submit() {
const deviceId = await this.kaidn.getDeviceId();
await fetch("/api/signup", {
method: "POST",
body: JSON.stringify({ email: this.email, deviceId }),
});
}
}Why this exists
Angular decorators have to be processed by Angular's own compiler. That is why a library
using them ships partial-Ivy output from ng-packagr and gets relinked against every
consumer's Angular version, and why such libraries break on majors.
This package contains no Angular decorators. It is built from runtime API only:
InjectionToken, inject, signal, and a factory provider. Plain tsc compiles it, there
is nothing to relink, and there is no version of Angular that can break the build.
The whole cost is the provideKaidn(…) line you were writing anyway.
What it gives you, and what it deliberately does not
You get a device id. No score, and no verdict, because the browser is an untrusted
place to make a decision: anything you render there, a determined visitor can edit. Send
the id to your server, score it with @kaidn/sdk
or any of the backend clients, and act on the verdict where nobody can reach it.
Reactive state, as signals
readonly deviceId = this.kaidn.deviceId; // Signal<string | null>
readonly isLoading = this.kaidn.isLoading; // Signal<boolean><button (click)="submit()" [disabled]="isLoading()">Create account</button>They are read-only signals. A consumer that could .set() them would be writing a device id
nothing collected.
SSR, and why there is no isPlatformBrowser check to write
getDeviceId() resolves to null when there is no window, so the same call is safe in a
component that renders on the server and the client. Nothing collects during server
rendering and there is no platform check for you to remember.
Collection happens when you ask
Nothing runs when the service is constructed. That is on purpose: collecting on demand puts the work at the moment somebody acts, which is when the signal is freshest, when consent is least ambiguous, and it keeps fingerprinting off every route of your app that has no signup on it.
It never throws
A blocked script, an ad blocker, an offline visitor, a browser that refuses the APIs: all
of it resolves to null.
const deviceId = await this.kaidn.getDeviceId(); // string | null, never a throwFail open. A fraud tool that can take down your signup form is a worse problem than the fraud it was meant to stop, so a missing id means your server scores the event with one signal fewer rather than your form breaking.
Watching a signed-in session
Some signals only exist across time, because they are a change rather than a value: a VPN that drops mid-session and leaks the real home IP, a session that starts masking partway through, one device seen from many addresses. None of them can be read from a single page load.
export class AccountLayoutComponent implements OnInit {
private readonly kaidn = inject(KaidnService);
private readonly destroyRef = inject(DestroyRef);
ngOnInit() {
this.kaidn.watchSession({ destroyRef: this.destroyRef });
}
}Pass the DestroyRef and the observation ends when the component does. It is asked for
explicitly rather than injected inside the service, because inject() only works in an
injection context and ngOnInit is not one — a method that silently required a context you
could not see would fail at runtime complaining about the wrong thing.
The beacon is free and you are billed per scored decision, so this adds signal without adding events.
Consent
provideKaidn({ publishableKey: "pk_live_…", consent: hasConsent ? "granted" : "denied" })With consent: "denied" nothing collects anything, and watchSession() hands back an inert
handle rather than nothing, so no caller has to branch on whether it got one. Fingerprinting
reads properties of a visitor's device, which in several jurisdictions needs a lawful basis
before it happens rather than a note in a policy afterwards.
Keys
The pk_live_… publishable key is meant to be visible in page source: it is
domain-locked and can only send fingerprints. Your kdn_live_… secret key is the opposite,
and passing one to provideKaidn throws where you wrote it rather than leaking into your
bundle.
Get a publishable key from the dashboard under Fraud Scoring API → Device trackers.
API
| | |
| --- | --- |
| provideKaidn({ publishableKey, endpoint?, consent }) | Provider[] for your app config. Validates the key immediately |
| KaidnService.getDeviceId({ timeoutMs? }) | Promise<string \| null>, never throws |
| KaidnService.deviceId / isLoading / error | read-only signals |
| KaidnService.watchSession({ intervalMs?, destroyRef? }) | { stop }. Re-beacons while a session is open |
| KAIDN_CONFIG | the injection token, if you need the resolved config yourself |
Requires Angular 16 or newer, for signals.
Links
- Angular quickstart
- Docs · Pricing: 10,000 events a month free, no card
MIT
