cometchat-angular-production — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited cometchat-angular-production (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Teaches Claude how to move a CometChat Angular UI Kit v5 (@cometchat/chat-uikit-angular@5) integration from dev-mode Auth Key to production-ready server-minted auth tokens + user CRUD. Covers:
authKey can't ship to productionCometChatUIKit.loginWithAuthToken(authToken) + token refreshCometChatUIKit.createUser / updateUser)environment.ts → environment.prod.ts) + production buildRead `cometchat-angular-core` first — production just swaps one call on the init/login flow, but understanding the standalone init → login → render lifecycle is the prerequisite.
Ground truth: @cometchat/[email protected] source (projects/cometchat-uikit/src/lib/cometchat-uikit.ts — the CometChatUIKit class with both static and injectable APIs), the sample-app login flow (projects/sample-app/src — main.ts init, services/auth.service.ts login/logout), docs/ui-kit/angular/integration.mdx + docs/fundamentals/user-auth, and the cross-platform REST API at https://{APP_ID}.api-{REGION}.cometchat.io/v3/. Verify any non-obvious symbol against the kit source before relying on it. Official docs: https://www.cometchat.com/docs/fundamentals/user-auth · Docs MCP: claude mcp add --transport http cometchat-docs https://www.cometchat.com/docs/mcp (or fetch the URL directly without MCP).
In dev mode, CometChatUIKit.login(uid) uses the authKey passed to UIKitSettingsBuilder.setAuthKey(). That key is bundled into your Angular JavaScript. Anyone can open DevTools → Sources → search for the key string and use it to log in as ANY user in your CometChat app — read private messages, send as other users, access every conversation.
Production MUST use server-side token generation:
CometChatUIKit.loginWithAuthToken(authToken).The dev setAuthKey() call MUST NOT ship in production builds. See §7 for the environment.prod.ts swap.
| Key | Where in dashboard | Purpose | Where it lives |
|---|---|---|---|
| Auth Key | API & Auth Keys → "Auth Keys" table | Client-side SDK login(uid) in dev mode | Client bundle — dev only. Never in production builds. |
| REST API Key | API & Auth Keys → "REST API Keys" table | Server-to-server: token generation, user CRUD | Server only. Never in environment.ts, environment.prod.ts, or any file under src/. |
If the project only has an Auth Key, the user needs to generate a REST API Key in the dashboard: API & Auth Keys → REST API Keys → Add Key. Pick "Full Access" for server-side use.
Why this matters more in Angular than elsewhere: Angular has no server runtime and no process.env split. Everything under src/ — including both environment.ts files — is compiled into the client bundle. There is no framework-prefix convention (NEXT_PUBLIC_, VITE_) that decides client vs server. So the rule is absolute: the REST API Key never appears in any Angular source file.
1. User logs into YOUR auth (Firebase Auth / Supabase / Clerk / Auth0 / custom)
↓
2. Angular app asks YOUR backend for a CometChat auth token
↓ (POST /api/cometchat-token with Authorization: Bearer <your-jwt>)
3. Backend calls CometChat REST API → gets an Auth Token for that UID
↓ POST https://{APP_ID}.api-{REGION}.cometchat.io/v3/users/{uid}/auth_tokens
with header apiKey: <REST_API_KEY>
↓
4. Angular calls CometChatUIKit.loginWithAuthToken(authToken)The Angular client never sees the REST API Key. The backend derives the UID from the authenticated session — never from the request body (see anti-patterns §9).
The CometChat REST API requires two headers on every server-to-server call:
appId — your CometChat app IDapiKey — your REST API Key (NOT the Auth Key used in dev mode)Angular projects have no built-in API routes. You need a separate backend. The token endpoint does the same thing in every recipe:
https://{APP_ID}.api-{REGION}.cometchat.io/v3/users/{uid}/auth_tokens with the apiKey header.{ authToken } to the client.// server/routes/cometchat-token.ts
import { Router } from "express";
import { requireAuth } from "../middleware/auth";
const router = Router();
const APP_ID = process.env.COMETCHAT_APP_ID!;
const REGION = process.env.COMETCHAT_REGION!;
const REST_API_KEY = process.env.COMETCHAT_REST_API_KEY!;
router.post("/cometchat-token", requireAuth, async (req, res) => {
const uid = req.user.id; // from authenticated session — NOT from request body
const r = await fetch(
`https://${APP_ID}.api-${REGION}.cometchat.io/v3/users/${encodeURIComponent(uid)}/auth_tokens`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
appId: APP_ID,
apiKey: REST_API_KEY,
},
body: JSON.stringify({}),
}
);
if (!r.ok) {
console.error("CometChat token error:", await r.text());
return res.status(r.status).json({ error: "Failed to generate auth token" });
}
const data = await r.json();
return res.json({ authToken: data.data.authToken });
});
export default router;Enable CORS for your Angular origin (e.g. http://localhost:4200 in dev) so the browser can call this endpoint:
import cors from "cors";
app.use(cors({ origin: "http://localhost:4200" }));import { Hono } from "hono";
const app = new Hono();
app.post("/api/cometchat-token", async (c) => {
const user = c.get("user");
if (!user) return c.json({ error: "unauthorized" }, 401);
const r = await fetch(
`https://${c.env.COMETCHAT_APP_ID}.api-${c.env.COMETCHAT_REGION}.cometchat.io/v3/users/${encodeURIComponent(user.id)}/auth_tokens`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
appId: c.env.COMETCHAT_APP_ID,
apiKey: c.env.COMETCHAT_REST_API_KEY,
},
body: JSON.stringify({}),
}
);
if (!r.ok) return c.json({ error: "token mint failed" }, 502);
const data = await r.json();
return c.json({ authToken: data.data.authToken });
});import { onCall, HttpsError } from "firebase-functions/v2/https";
export const getCometChatToken = onCall(
{ secrets: ["COMETCHAT_APP_ID", "COMETCHAT_REGION", "COMETCHAT_REST_API_KEY"] },
async (request) => {
if (!request.auth) throw new HttpsError("unauthenticated", "Sign in required");
const uid = request.auth.uid;
const r = await fetch(
`https://${process.env.COMETCHAT_APP_ID}.api-${process.env.COMETCHAT_REGION}.cometchat.io/v3/users/${encodeURIComponent(uid)}/auth_tokens`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
appId: process.env.COMETCHAT_APP_ID!,
apiKey: process.env.COMETCHAT_REST_API_KEY!,
},
body: JSON.stringify({}),
}
);
if (!r.ok) throw new HttpsError("internal", "token mint failed");
const data = await r.json();
return { authToken: data.data.authToken };
}
);// pages/api/cometchat-token.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") return res.status(405).end();
const session = await getServerSession(req, res, authOptions);
if (!session?.user) return res.status(401).json({ error: "unauthorized" });
const uid = session.user.id;
const r = await fetch(
`https://${process.env.COMETCHAT_APP_ID}.api-${process.env.COMETCHAT_REGION}.cometchat.io/v3/users/${encodeURIComponent(uid)}/auth_tokens`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
appId: process.env.COMETCHAT_APP_ID!,
apiKey: process.env.COMETCHAT_REST_API_KEY!,
},
body: JSON.stringify({}),
}
);
if (!r.ok) return res.status(502).json({ error: "token mint failed" });
const data = await r.json();
return res.json({ authToken: data.data.authToken });
}The v5 production login call is `CometChatUIKit.loginWithAuthToken(authToken)` — it takes a bare token string, the same way v5 dev login takes a bare uid string (CometChatUIKit.login(uid)). Do NOT pass an object — loginWithAuthToken({ authToken }) is a v4-ism and will not compile against the v5 types (verified: static loginWithAuthToken(authToken: string): Promise<CometChat.User>).
// cometchat-auth.service.ts
import { Injectable } from "@angular/core";
import { HttpClient, HttpHeaders } from "@angular/common/http";
import { CometChatUIKit } from "@cometchat/chat-uikit-angular";
import { environment } from "../environments/environment";
import { firstValueFrom } from "rxjs";
@Injectable({ providedIn: "root" })
export class CometChatAuthService {
constructor(private http: HttpClient) {}
async loginWithToken(appJwt: string): Promise<void> {
// 1. Already logged in? getLoggedInUser() (capital I) is the SYNC v5 accessor —
// returns CometChat.User | null from the cached BehaviorSubject, no await.
// (There is also an async getLoggedinUser() — lowercase i — returning
// Promise<CometChat.User | null>; the sample-app route guards use that one.
// Both are real v5 symbols; pick sync for a fast already-logged-in short-circuit.)
if (CometChatUIKit.getLoggedInUser()) return;
// 2. Fetch a CometChat auth token from your backend.
const response = await firstValueFrom(
this.http.post<{ authToken: string }>(
environment.cometchat.tokenEndpoint,
{},
{ headers: new HttpHeaders({ Authorization: `Bearer ${appJwt}` }) }
)
);
// 3. Log in with the bare token string (v5 signature).
await CometChatUIKit.loginWithAuthToken(response.authToken);
}
async logout(): Promise<void> {
await CometChatUIKit.logout();
}
}// app.component.ts — production-aware init
import { Component, OnInit } from "@angular/core";
import { CometChatAuthService } from "./cometchat-auth.service";
import { YourAuthService } from "./your-auth.service"; // your existing auth
@Component({
selector: "app-root",
standalone: true,
templateUrl: "./app.component.html",
})
export class AppComponent implements OnInit {
isReady = false;
constructor(
private cometChatAuth: CometChatAuthService,
private yourAuth: YourAuthService
) {}
ngOnInit(): void {
// CometChatUIKit.init() already ran via APP_INITIALIZER (see cometchat-angular-core).
this.yourAuth
.getJwt()
.then((jwt) => this.cometChatAuth.loginWithToken(jwt))
.then(() => (this.isReady = true))
.catch(console.error);
}
}Gate the chat UI on isReady so no <cometchat-*> component renders before loginWithAuthToken resolves.
CometChat auth tokens expire. When a token expires, the SDK disconnects. Re-mint and re-login through a single in-flight guard so concurrent triggers don't fire two logins (the SDK throws "Please wait until the previous login request ends." on a racing second loginWithAuthToken).
// In CometChatAuthService — module/instance-level guard.
private refreshInFlight: Promise<void> | null = null;
async refreshSession(appJwt: string): Promise<void> {
if (this.refreshInFlight) {
await this.refreshInFlight; // another caller already triggered a refresh
return;
}
this.refreshInFlight = (async () => {
const response = await firstValueFrom(
this.http.post<{ authToken: string }>(
environment.cometchat.tokenEndpoint,
{},
{ headers: new HttpHeaders({ Authorization: `Bearer ${appJwt}` }) }
)
);
await CometChatUIKit.loginWithAuthToken(response.authToken);
})();
try {
await this.refreshInFlight;
} finally {
this.refreshInFlight = null;
}
}Wire refreshSession to a CometChat.addConnectionListener(...) onDisconnected handler, or simply re-mint and re-login whenever a CometChat operation returns an auth error.
In production you keep CometChat users in sync with your app's users. There are two paths:
The production path is the REST API (server-side, REST API Key). Create the CometChat user the moment the app user signs up — otherwise loginWithAuthToken fails with "user does not exist."
The client helpers exist too — CometChatUIKit.createUser(user) and CometChatUIKit.updateUser(user) are exported by the v5 kit (verified: both (user: CometChat.User): Promise<CometChat.User>). But they require the Auth Key on the UIKitSettings to authorize, which is exactly what you remove in production. Treat them as a dev/admin-tool convenience, not the production user-provisioning mechanism. Production provisioning belongs server-side via the REST API. (Note: the kit exposes createUser/updateUser only — there is no client-side deleteUser; deletion is REST-only.)
If you do use the client helper (e.g. a one-off admin screen running with the Auth Key), the shape is a CometChat.User:
import { CometChat } from "@cometchat/chat-sdk-javascript";
import { CometChatUIKit } from "@cometchat/chat-uikit-angular";
const user = new CometChat.User("user-uid-1");
user.setName("Alice");
await CometChatUIKit.createUser(user); // Promise<CometChat.User>async function createCometChatUser(uid: string, name: string, avatarUrl?: string) {
const r = await fetch(`https://${APP_ID}.api-${REGION}.cometchat.io/v3/users`, {
method: "POST",
headers: {
"Content-Type": "application/json",
appId: APP_ID,
apiKey: REST_API_KEY,
},
body: JSON.stringify({ uid, name, ...(avatarUrl ? { avatar: avatarUrl } : {}) }),
});
if (!r.ok) {
// 409 = user already exists — safe to ignore on re-signup.
if (r.status === 409) return;
throw new Error(`CometChat user create failed: ${await r.text()}`);
}
return r.json();
}async function updateCometChatUser(uid: string, updates: { name?: string; avatar?: string }) {
const r = await fetch(
`https://${APP_ID}.api-${REGION}.cometchat.io/v3/users/${encodeURIComponent(uid)}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
appId: APP_ID,
apiKey: REST_API_KEY,
},
body: JSON.stringify(updates),
}
);
if (!r.ok) throw new Error(`CometChat user update failed: ${await r.text()}`);
return r.json();
}async function deleteCometChatUser(uid: string) {
const r = await fetch(
`https://${APP_ID}.api-${REGION}.cometchat.io/v3/users/${encodeURIComponent(uid)}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
appId: APP_ID,
apiKey: REST_API_KEY,
},
body: JSON.stringify({ permanent: true }),
}
);
if (!r.ok) throw new Error(`CometChat user delete failed: ${await r.text()}`);
}functions.auth.user().onCreate(...) → createCometChatUser; .onDelete(...) → deleteCometChatUser. Firebase fires no event on profile change, so call updateCometChatUser from your profile-update endpoint.auth.users (INSERT/UPDATE/DELETE) → the matching CRUD helper.user.created / user.updated / user.deleted webhook events (verify the svix signature first).createCometChatUser.Angular has no .env / process.env. Config lives in src/environments/. The production build (ng build --configuration production) swaps environment.ts → environment.prod.ts via the fileReplacements in angular.json. The split is your only lever — anything in the active file ships to the client.
// src/environments/environment.ts (development)
export const environment = {
production: false,
cometchat: {
appId: "YOUR_APP_ID",
region: "us", // "us" | "eu" | "in"
authKey: "YOUR_AUTH_KEY", // dev only — must NOT exist in environment.prod.ts
},
};// src/environments/environment.prod.ts (production)
export const environment = {
production: true,
cometchat: {
appId: "YOUR_APP_ID",
region: "us",
// No authKey — mint auth tokens server-side and call loginWithAuthToken().
// No REST API Key — it lives on your backend only.
tokenEndpoint: "https://api.yourapp.com/cometchat-token",
},
};"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
]
}
}Because environment.prod.ts has no authKey, the UIKitSettingsBuilder must not call .setAuthKey() in a production build. Guard it:
const builder = new UIKitSettingsBuilder()
.setAppId(environment.cometchat.appId)
.setRegion(environment.cometchat.region)
.subscribePresenceForAllUsers();
// Dev only — the prod environment has no authKey, so this branch is skipped.
if (!environment.production && "authKey" in environment.cometchat) {
builder.setAuthKey((environment.cometchat as any).authKey);
}
await CometChatUIKit.init(builder.build());| Variable | Location | Visibility |
|---|---|---|
appId | environment.ts + environment.prod.ts + backend | OK client-side |
region | environment.ts + environment.prod.ts + backend | OK client-side |
authKey | Dev `environment.ts` only. Absent from environment.prod.ts. | Must NEVER ship in a production Angular build |
tokenEndpoint | environment.prod.ts | Your backend URL — safe in client bundle |
| REST API Key | Backend env only. Never under src/. | Never in any Angular file, ever |
Before releasing to production, verify every item:
authKey removed from environment.prod.ts.UIKitSettingsBuilder without .setAuthKey().CometChatUIKit.loginWithAuthToken(authToken) (bare string), not login(uid).grep -rIE "REST_API_KEY|restApiKey" src/ (must return nothing).loginWithAuthToken fails with "user does not exist."environment.ts → environment.prod.ts (check fileReplacements in angular.json).environment.ts, environment.prod.ts, assets/, or any TypeScript under src/. Angular bundles everything in src/ into client JavaScript. There is no process.env split to save you.POST /cometchat-token { uid: "..." } that trusts the body is equivalent to no auth — any user can impersonate any other.loginWithAuthToken(token), mirroring login(uid). loginWithAuthToken({ authToken }) is a v4-era shape and won't typecheck against v5.uid login requires the Auth Key on the UIKitSettings. In production set neither .setAuthKey() on the builder nor call login(uid) — use loginWithAuthToken(token).deleteUser — deletion is REST-only.)loginWithAuthToken.| Skill | When to route |
|---|---|
cometchat-angular-core | Init / login / standalone setup / environment — prerequisite |
cometchat-angular-components | Base component props |
cometchat-angular-placement | Where your chat UI goes |
cometchat-angular-patterns | Standalone wiring, APP_INITIALIZER, auth guard |
cometchat-angular-theming | CSS-variable theme customization |
cometchat-angular-features | Feature flags |
cometchat-angular-calls | Voice/video calling |
cometchat-angular-customization | Customization tied to server-side data |
cometchat-angular-production | This skill — server tokens + user CRUD |
cometchat-angular-troubleshooting | 401 on token fetch, "user does not exist" on login |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.