/**
* Generic seed context carried by an openable notification. The front-end trusts
* these keys to reconstruct the tapped notification's chat, so they travel with
* the payload rather than being re-derived downstream.
*
* `notificationId`, `subtype`, `createdAt`, and `contextText` are always present.
* `address`/`asset`/`dex`/`traderAddress` are optional: runtime alerts are
* strategy-scoped (no asset), discovery is trader-scoped (no strategy address),
* so those fields may legitimately be absent.
*/
export interface NotificationContext {
notificationId: string;
subtype: string;
createdAt: string;
contextText: string;
address?: string;
asset?: string;
dex?: string;
traderAddress?: string;
}
/**
* Push/in-app copy for a notification.
*
* There is no push title here: the notification service owns the push title and
* sets it to the resolved strategy/skill name (looked up from
* `actionMetadata.strategyWalletAddress`). The body therefore says only WHAT
* happened and must not repeat the strategy name.
*/
export interface NotificationCopy {
/** Push body, plain text (no markup); ≤128 after truncation. */
pushBody: string;
/** In-app text, label-free (the front-end renders the label as its own tag); may carry literal `
` line breaks the front-end renders as newlines. */
inApp: string;
}
/** Hard limit the notification service enforces: an over-length body is dropped, not truncated server-side. */
export declare const PUSH_BODY_MAX = 128;
/** Truncate to `max` UTF-16 units, appending "…" when clipped. Never splits a
* surrogate pair: if the cut would land between an emoji's two units, it backs off
* one so the result ends on a whole character rather than a lone `�`. */
export declare function truncateForPush(s: string, max: number): string;
/** Fields common to every notification payload, openable or not. */
interface NotificationPayloadBase {
message: string;
recipientUserId?: string;
subtype?: string;
contextText?: string;
/** Push/in-app copy block; when absent the envelope builder derives a fallback from `message`. */
copy?: NotificationCopy;
/**
* Wallet address of the strategy this notification is about. The notification
* service resolves the displayed strategy/skill name from this field and
* nowhere else, so it must be set whenever the address is known — regardless
* of `canOpenAgentChat`. `context.address` is a different concern (the chat
* seed key) and is dropped on the non-openable path; this one is not.
* Omitted only when the address is genuinely unknown.
*/
strategyWalletAddress?: string;
/**
* Semantic (content-derived) dedupe keys. When set, they REPLACE the default
* `actionMetadata.uniqueKeys = [notificationId]`, so the downstream ingest can
* collapse near-duplicate alerts that describe the same real-world event (e.g.
* two "Stop raised" for the same position/floor from two runtime processes).
* `notificationId` stays a fresh UUID — it is the ledger row id and must remain
* unique. Omitted by every family that wants only transport-retry dedupe.
*/
uniqueKeys?: string[];
/**
* Semantic idempotency key. When set, it REPLACES the default
* `idempotencyKey = notificationId` on the wire, so the ingest dedupes on the
* content identity rather than the per-event row id. Set together with
* {@link uniqueKeys} for the content-dedupe families; omitted otherwise.
*/
idempotencyKey?: string;
}
/**
* Structured notification payload, discriminated on `canOpenAgentChat`.
*
* When `canOpenAgentChat` is `true` the generic seed `context` is REQUIRED — the
* compiler catches an openable-without-context at the producer, before the flag
* reaches a front-end that trusts it. When `false`, `context` is optional.
*
* Openability is opt-in and fail-safe: an absent flag is never treated as `true`.
*/
export type NotificationPayload = (NotificationPayloadBase & {
canOpenAgentChat: true;
context: NotificationContext;
}) | (NotificationPayloadBase & {
canOpenAgentChat: false;
context?: NotificationContext;
});
export interface NotificationService {
notify(input: string | NotificationPayload): Promise;
}
/**
* Normalize a notify() input to a structured payload.
*
* A bare string maps to a non-openable payload carrying that message. A structured
* payload passes through (copied, never mutated). Openability is fail-safe: only
* the literal boolean `true` enables the tap — absent, `undefined`, or any junk
* value resolves to `false`. Never `Boolean(...)`-coerce, which would flip a
* truthy non-boolean (or a truthy message) into openable.
*/
export declare function toPayload(input: string | NotificationPayload): NotificationPayload;
export {};
//# sourceMappingURL=notification.d.ts.map