@xtremax/pigeon 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +56 -2
- package/dist/cjs/browser-stub.js +10 -0
- package/dist/cjs/client.js +112 -0
- package/dist/cjs/contract.js +5 -0
- package/dist/cjs/errors.js +48 -0
- package/dist/cjs/events.js +62 -0
- package/dist/cjs/idempotency.js +14 -0
- package/dist/cjs/inbox/browser-stub.js +10 -0
- package/dist/cjs/inbox/client.js +39 -0
- package/dist/cjs/inbox/errors.js +44 -0
- package/dist/cjs/inbox/handler.js +127 -0
- package/dist/cjs/inbox/index.js +15 -0
- package/dist/cjs/inbox/origin.js +22 -0
- package/dist/cjs/inbox/project.js +84 -0
- package/dist/cjs/inbox/validate.js +59 -0
- package/dist/cjs/inbox-react/format.js +72 -0
- package/dist/cjs/inbox-react/inbox-client.js +63 -0
- package/dist/cjs/inbox-react/inbox-context.js +13 -0
- package/dist/cjs/inbox-react/inbox-full-list.js +13 -0
- package/dist/cjs/inbox-react/inbox-list.js +37 -0
- package/dist/cjs/inbox-react/inbox-panel.js +13 -0
- package/dist/cjs/inbox-react/inbox-provider.js +157 -0
- package/dist/cjs/inbox-react/inbox-state.js +88 -0
- package/dist/cjs/inbox-react/index.js +25 -0
- package/dist/cjs/inbox-react/labels.js +38 -0
- package/dist/cjs/inbox-react/notification-bell.js +32 -0
- package/dist/cjs/inbox-react/notification-item.js +17 -0
- package/dist/cjs/inbox-react/parse.js +94 -0
- package/dist/cjs/inbox-react/preference-dialog.js +49 -0
- package/dist/cjs/inbox-react/types.js +4 -0
- package/dist/cjs/inbox-react/use-inbox-polling.js +94 -0
- package/dist/cjs/index.js +14 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/retry.js +14 -0
- package/dist/esm/browser-stub.js +6 -0
- package/dist/esm/client.js +107 -0
- package/dist/esm/contract.js +2 -0
- package/dist/esm/errors.js +42 -0
- package/dist/esm/events.js +57 -0
- package/dist/esm/idempotency.js +10 -0
- package/dist/esm/inbox/browser-stub.js +7 -0
- package/dist/esm/inbox/client.js +35 -0
- package/dist/esm/inbox/errors.js +38 -0
- package/dist/esm/inbox/handler.js +124 -0
- package/dist/esm/inbox/index.js +5 -0
- package/dist/esm/inbox/origin.js +19 -0
- package/dist/esm/inbox/project.js +76 -0
- package/dist/esm/inbox/validate.js +49 -0
- package/dist/esm/inbox-react/format.js +64 -0
- package/dist/esm/inbox-react/inbox-client.js +58 -0
- package/dist/esm/inbox-react/inbox-context.js +9 -0
- package/dist/esm/inbox-react/inbox-full-list.js +10 -0
- package/dist/esm/inbox-react/inbox-list.js +34 -0
- package/dist/esm/inbox-react/inbox-panel.js +10 -0
- package/dist/esm/inbox-react/inbox-provider.js +154 -0
- package/dist/esm/inbox-react/inbox-state.js +83 -0
- package/dist/esm/inbox-react/index.js +18 -0
- package/dist/esm/inbox-react/labels.js +34 -0
- package/dist/esm/inbox-react/notification-bell.js +29 -0
- package/dist/esm/inbox-react/notification-item.js +14 -0
- package/dist/esm/inbox-react/parse.js +85 -0
- package/dist/esm/inbox-react/preference-dialog.js +46 -0
- package/dist/esm/inbox-react/types.js +1 -0
- package/dist/esm/inbox-react/use-inbox-polling.js +88 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/retry.js +9 -0
- package/dist/inbox-react/styles.css +65 -0
- package/dist/types/browser-stub.d.ts +4 -0
- package/dist/types/client.d.ts +94 -0
- package/dist/types/contract.d.ts +182 -0
- package/dist/types/errors.d.ts +29 -0
- package/dist/types/events.d.ts +26 -0
- package/dist/types/idempotency.d.ts +4 -0
- package/dist/types/inbox/browser-stub.d.ts +3 -0
- package/dist/types/inbox/client.d.ts +34 -0
- package/dist/types/inbox/errors.d.ts +13 -0
- package/dist/types/inbox/handler.d.ts +25 -0
- package/dist/types/inbox/index.d.ts +6 -0
- package/dist/types/inbox/origin.d.ts +5 -0
- package/dist/types/inbox/project.d.ts +7 -0
- package/dist/types/inbox/validate.d.ts +25 -0
- package/dist/types/inbox-react/format.d.ts +13 -0
- package/dist/types/inbox-react/inbox-client.d.ts +24 -0
- package/dist/types/inbox-react/inbox-context.d.ts +29 -0
- package/dist/types/inbox-react/inbox-full-list.d.ts +7 -0
- package/dist/types/inbox-react/inbox-list.d.ts +12 -0
- package/dist/types/inbox-react/inbox-panel.d.ts +2 -0
- package/dist/types/inbox-react/inbox-provider.d.ts +18 -0
- package/dist/types/inbox-react/inbox-state.d.ts +70 -0
- package/dist/types/inbox-react/index.d.ts +19 -0
- package/dist/types/inbox-react/labels.d.ts +40 -0
- package/dist/types/inbox-react/notification-bell.d.ts +4 -0
- package/dist/types/inbox-react/notification-item.d.ts +9 -0
- package/dist/types/inbox-react/parse.d.ts +9 -0
- package/dist/types/inbox-react/preference-dialog.d.ts +5 -0
- package/dist/types/inbox-react/types.d.ts +55 -0
- package/dist/types/inbox-react/use-inbox-polling.d.ts +33 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/retry.d.ts +13 -0
- package/package.json +33 -4
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { AcceptedMessage, ChannelInfo, MessageView, PingInfo, RecipientView, SendMessageRequest } from './contract.js';
|
|
2
|
+
import { type RetryPolicy } from './retry.js';
|
|
3
|
+
export interface ClientOptions {
|
|
4
|
+
/** Defaults to `process.env.COMMUNICATION_API_KEY`. */
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
/** Defaults to `process.env.COMMUNICATION_URL`. */
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
fetch?: typeof fetch;
|
|
9
|
+
retry?: Partial<RetryPolicy>;
|
|
10
|
+
/** Injectable for tests. */
|
|
11
|
+
sleep?: (ms: number) => Promise<void>;
|
|
12
|
+
random?: () => number;
|
|
13
|
+
}
|
|
14
|
+
/** Per-call choices. `retry: false` makes one attempt; `timeoutMs` bounds the whole call; `headers` never replace the key. */
|
|
15
|
+
export interface RequestOptions {
|
|
16
|
+
retry?: boolean;
|
|
17
|
+
headers?: Record<string, string>;
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
}
|
|
20
|
+
export type Transport = <T>(method: string, path: string, body?: unknown, opts?: RequestOptions) => Promise<{
|
|
21
|
+
data: T;
|
|
22
|
+
headers: Headers;
|
|
23
|
+
}>;
|
|
24
|
+
/** Where a client keeps its transport, so `@xtremax/pigeon/inbox` can reuse the key handling, retries and errors. */
|
|
25
|
+
export declare const TRANSPORT: unique symbol;
|
|
26
|
+
export declare function createCommunicationClient(options?: ClientOptions): {
|
|
27
|
+
messages: {
|
|
28
|
+
/** Sends are retried safely because every request carries an idempotency key (yours, or a random one). */
|
|
29
|
+
send(req: SendMessageRequest): Promise<AcceptedMessage>;
|
|
30
|
+
get: (id: string) => Promise<MessageView>;
|
|
31
|
+
};
|
|
32
|
+
channels: {
|
|
33
|
+
list: () => Promise<ChannelInfo[]>;
|
|
34
|
+
};
|
|
35
|
+
ping: () => Promise<PingInfo>;
|
|
36
|
+
recipients: {
|
|
37
|
+
put: (personId: string, body: {
|
|
38
|
+
timezone?: string | null;
|
|
39
|
+
locale?: string | null;
|
|
40
|
+
quietHours?: {
|
|
41
|
+
start: string;
|
|
42
|
+
end: string;
|
|
43
|
+
} | null;
|
|
44
|
+
}) => Promise<RecipientView>;
|
|
45
|
+
get: (personId: string) => Promise<RecipientView>;
|
|
46
|
+
delete(personId: string): Promise<void>;
|
|
47
|
+
putAddress: (personId: string, body: {
|
|
48
|
+
medium: "email" | "whatsapp";
|
|
49
|
+
address: string;
|
|
50
|
+
primary?: boolean;
|
|
51
|
+
verified?: boolean;
|
|
52
|
+
}) => Promise<{
|
|
53
|
+
id: string;
|
|
54
|
+
masked: string;
|
|
55
|
+
}>;
|
|
56
|
+
deleteAddress(personId: string, addressId: string): Promise<void>;
|
|
57
|
+
};
|
|
58
|
+
preferences: {
|
|
59
|
+
record: (body: {
|
|
60
|
+
recipient: {
|
|
61
|
+
personId?: string;
|
|
62
|
+
address?: string;
|
|
63
|
+
medium?: "email" | "whatsapp";
|
|
64
|
+
};
|
|
65
|
+
medium: "email" | "whatsapp" | "in_app";
|
|
66
|
+
state: "opted_in" | "opted_out";
|
|
67
|
+
expressedAt?: string;
|
|
68
|
+
evidence?: {
|
|
69
|
+
method?: string;
|
|
70
|
+
wordingVersion?: string;
|
|
71
|
+
proofRef?: string;
|
|
72
|
+
};
|
|
73
|
+
}) => Promise<{
|
|
74
|
+
changed: boolean;
|
|
75
|
+
current: unknown;
|
|
76
|
+
}>;
|
|
77
|
+
};
|
|
78
|
+
messageClasses: {
|
|
79
|
+
put: (templateKey: string, body: {
|
|
80
|
+
class: "transactional" | "non_transactional";
|
|
81
|
+
note?: string;
|
|
82
|
+
}) => Promise<unknown>;
|
|
83
|
+
list: () => Promise<{
|
|
84
|
+
templateKey: string;
|
|
85
|
+
class: string;
|
|
86
|
+
note: string | null;
|
|
87
|
+
}[]>;
|
|
88
|
+
};
|
|
89
|
+
/** For logs: never prints the key. */
|
|
90
|
+
describe: () => string;
|
|
91
|
+
[TRANSPORT]: Transport;
|
|
92
|
+
};
|
|
93
|
+
export type CommunicationClient = ReturnType<typeof createCommunicationClient>;
|
|
94
|
+
export { CommunicationError } from './errors.js';
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/** Types mirrored from the Gateway's external API (types only; safe in any bundle). */
|
|
2
|
+
export declare const SUPPORTED_API_VERSION = "v1";
|
|
3
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
4
|
+
[key: string]: JsonValue;
|
|
5
|
+
};
|
|
6
|
+
export type MessageState = 'queued' | 'sent' | 'delivered' | 'read' | 'failed';
|
|
7
|
+
export type FailureCategory = 'invalid_recipient' | 'recipient_unreachable' | 'recipient_blocked' | 'content_rejected' | 'provider_rejected' | 'provider_unavailable' | 'rate_limited' | 'configuration_error' | 'policy_refused' | 'retries_exhausted' | 'unknown';
|
|
8
|
+
export interface NeutralReason {
|
|
9
|
+
category: FailureCategory;
|
|
10
|
+
code: string;
|
|
11
|
+
message: string;
|
|
12
|
+
}
|
|
13
|
+
export type ApiErrorCode = 'invalid_json' | 'invalid_credentials' | 'forbidden' | 'not_found' | 'idempotency_conflict' | 'payload_too_large' | 'unsupported_media_type' | 'validation_failed' | 'channel_unavailable' | 'tls_required' | 'rate_limited' | 'internal_error' | 'temporarily_unavailable' | 'conflict';
|
|
14
|
+
export interface ApiErrorDetail {
|
|
15
|
+
field?: string;
|
|
16
|
+
code?: string;
|
|
17
|
+
message?: string;
|
|
18
|
+
reason?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ApiErrorBody {
|
|
21
|
+
error: {
|
|
22
|
+
code: string;
|
|
23
|
+
message: string;
|
|
24
|
+
requestId?: string;
|
|
25
|
+
details?: ApiErrorDetail[];
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export interface SendMessageRequest {
|
|
29
|
+
templateKey: string;
|
|
30
|
+
channel: string;
|
|
31
|
+
recipient: {
|
|
32
|
+
personId?: string;
|
|
33
|
+
address?: string;
|
|
34
|
+
};
|
|
35
|
+
variables?: Record<string, JsonValue>;
|
|
36
|
+
/** Optional: the SDK derives a stable key from the request when you leave it out. */
|
|
37
|
+
idempotencyKey?: string;
|
|
38
|
+
reference?: JsonValue;
|
|
39
|
+
locale?: string;
|
|
40
|
+
options?: {
|
|
41
|
+
testOutcome?: 'accepted' | 'delivered' | 'read' | 'transient_failure' | 'permanent_failure';
|
|
42
|
+
succeedOnAttempt?: number;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export interface AcceptedMessage {
|
|
46
|
+
id: string;
|
|
47
|
+
state: MessageState;
|
|
48
|
+
channel: string;
|
|
49
|
+
reference: JsonValue | null;
|
|
50
|
+
acceptedAt: string;
|
|
51
|
+
idempotentReplay: boolean;
|
|
52
|
+
}
|
|
53
|
+
export interface MessageView {
|
|
54
|
+
id: string;
|
|
55
|
+
state: MessageState;
|
|
56
|
+
channel: string;
|
|
57
|
+
templateKey: string;
|
|
58
|
+
reference: JsonValue | null;
|
|
59
|
+
acceptedAt: string;
|
|
60
|
+
test: boolean;
|
|
61
|
+
failure: NeutralReason | null;
|
|
62
|
+
history: Array<{
|
|
63
|
+
sequence: number;
|
|
64
|
+
previousState: MessageState | null;
|
|
65
|
+
state: MessageState;
|
|
66
|
+
occurredAt: string;
|
|
67
|
+
cause: string;
|
|
68
|
+
reason: NeutralReason | null;
|
|
69
|
+
}>;
|
|
70
|
+
attempts: Array<{
|
|
71
|
+
attempt: number;
|
|
72
|
+
startedAt: string;
|
|
73
|
+
finishedAt: string | null;
|
|
74
|
+
outcome: 'in_progress' | 'accepted' | 'transient_failure' | 'permanent_failure' | 'interrupted';
|
|
75
|
+
reasonCode: string | null;
|
|
76
|
+
}>;
|
|
77
|
+
}
|
|
78
|
+
export interface ChannelInfo {
|
|
79
|
+
key: string;
|
|
80
|
+
name: string;
|
|
81
|
+
medium: 'email' | 'whatsapp' | 'in_app';
|
|
82
|
+
owner: 'shared' | 'tenant';
|
|
83
|
+
ready: boolean;
|
|
84
|
+
reason: string | null;
|
|
85
|
+
}
|
|
86
|
+
export interface PingInfo {
|
|
87
|
+
clientId: string;
|
|
88
|
+
tenantKey: string; /** The sub-tenant this key belongs to, or null at tenant level. The in-app inbox shows a person what this sub-tenant sent them. */
|
|
89
|
+
subKey?: string | null;
|
|
90
|
+
scopes: string[];
|
|
91
|
+
}
|
|
92
|
+
export interface MessageStateChangedEvent {
|
|
93
|
+
eventId: string;
|
|
94
|
+
type: 'message.state_changed';
|
|
95
|
+
version: 1;
|
|
96
|
+
sequence: number;
|
|
97
|
+
messageId: string;
|
|
98
|
+
reference: JsonValue | null;
|
|
99
|
+
channel: string;
|
|
100
|
+
previousState: MessageState;
|
|
101
|
+
state: MessageState;
|
|
102
|
+
occurredAt: string;
|
|
103
|
+
reason: {
|
|
104
|
+
category: FailureCategory;
|
|
105
|
+
code: string;
|
|
106
|
+
message: string;
|
|
107
|
+
} | null;
|
|
108
|
+
test: boolean;
|
|
109
|
+
}
|
|
110
|
+
export interface RecipientView {
|
|
111
|
+
personId: string;
|
|
112
|
+
status: 'active' | 'erased';
|
|
113
|
+
timezone: string | null;
|
|
114
|
+
locale: string | null;
|
|
115
|
+
quietHours: {
|
|
116
|
+
start: string;
|
|
117
|
+
end: string;
|
|
118
|
+
} | null;
|
|
119
|
+
addresses: Array<{
|
|
120
|
+
id: string;
|
|
121
|
+
medium: string;
|
|
122
|
+
masked: string;
|
|
123
|
+
primary: boolean;
|
|
124
|
+
verifiedAt: string | null;
|
|
125
|
+
}>;
|
|
126
|
+
preferences: Array<{
|
|
127
|
+
medium: string;
|
|
128
|
+
state: 'opted_in' | 'opted_out' | 'not_set';
|
|
129
|
+
changedAt: string | null;
|
|
130
|
+
source: string | null;
|
|
131
|
+
}>;
|
|
132
|
+
}
|
|
133
|
+
export interface InboxNotification {
|
|
134
|
+
/** The inbox item, not the message. */
|
|
135
|
+
id: string;
|
|
136
|
+
kind: 'notification' | 'announcement';
|
|
137
|
+
/** Plain text. */
|
|
138
|
+
title: string;
|
|
139
|
+
body: string;
|
|
140
|
+
/** Absolute https, or null. */
|
|
141
|
+
link: string | null;
|
|
142
|
+
source: {
|
|
143
|
+
type: 'xapp' | 'tenant';
|
|
144
|
+
name: string;
|
|
145
|
+
};
|
|
146
|
+
createdAt: string;
|
|
147
|
+
read: boolean;
|
|
148
|
+
readAt: string | null;
|
|
149
|
+
}
|
|
150
|
+
export interface InboxSummary {
|
|
151
|
+
unreadCount: number;
|
|
152
|
+
newestUnreadAt: string | null;
|
|
153
|
+
pollSeconds: number;
|
|
154
|
+
}
|
|
155
|
+
export interface InboxNotificationPage {
|
|
156
|
+
notifications: InboxNotification[];
|
|
157
|
+
nextCursor: string | null;
|
|
158
|
+
unreadCount: number;
|
|
159
|
+
}
|
|
160
|
+
export interface InboxPreference {
|
|
161
|
+
state: 'opted_in' | 'opted_out' | 'not_set';
|
|
162
|
+
changedAt: string | null;
|
|
163
|
+
delivers: boolean;
|
|
164
|
+
}
|
|
165
|
+
export interface InboxMarkReadResult {
|
|
166
|
+
notification: InboxNotification;
|
|
167
|
+
unreadCount: number;
|
|
168
|
+
}
|
|
169
|
+
export interface InboxReadAllResult {
|
|
170
|
+
marked: number;
|
|
171
|
+
remaining: number;
|
|
172
|
+
unreadCount: number;
|
|
173
|
+
}
|
|
174
|
+
/** What the host's browser-facing inbox routes answer when a call fails (IB-8). */
|
|
175
|
+
export type InboxHostErrorCode = 'not_signed_in' | 'not_permitted' | 'not_found' | 'invalid_input' | 'rate_limited' | 'unavailable';
|
|
176
|
+
export interface InboxHostErrorBody {
|
|
177
|
+
error: {
|
|
178
|
+
code: InboxHostErrorCode;
|
|
179
|
+
message: string;
|
|
180
|
+
field?: string;
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ApiErrorCode, ApiErrorDetail } from './contract.js';
|
|
2
|
+
export type ErrorCode = ApiErrorCode | 'unknown_error' | 'network_error' | 'timeout' | 'aborted' | 'invalid_response' | 'not_configured' | 'server_only';
|
|
3
|
+
/** Removes anything shaped like a key from text that may be logged. */
|
|
4
|
+
export declare const redact: (text: string) => string;
|
|
5
|
+
export declare class CommunicationError extends Error {
|
|
6
|
+
readonly code: ErrorCode;
|
|
7
|
+
readonly apiCode: string | null;
|
|
8
|
+
readonly status: number | null;
|
|
9
|
+
readonly requestId: string | null;
|
|
10
|
+
readonly details: readonly ApiErrorDetail[];
|
|
11
|
+
readonly retryable: boolean;
|
|
12
|
+
readonly retryAfterSeconds: number | null;
|
|
13
|
+
readonly attempts: number;
|
|
14
|
+
constructor(init: {
|
|
15
|
+
code: ErrorCode;
|
|
16
|
+
message: string;
|
|
17
|
+
apiCode?: string | null;
|
|
18
|
+
status?: number | null;
|
|
19
|
+
requestId?: string | null;
|
|
20
|
+
details?: readonly ApiErrorDetail[];
|
|
21
|
+
retryable?: boolean;
|
|
22
|
+
retryAfterSeconds?: number | null;
|
|
23
|
+
attempts?: number;
|
|
24
|
+
});
|
|
25
|
+
toJSON(): Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
export declare function fromResponse(status: number, body: unknown, headers: {
|
|
28
|
+
get(n: string): string | null;
|
|
29
|
+
}, attempts: number): CommunicationError;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { MessageStateChangedEvent } from './contract.js';
|
|
2
|
+
export declare const DEFAULT_TOLERANCE_SECONDS = 300;
|
|
3
|
+
export type VerifyResult = {
|
|
4
|
+
valid: true;
|
|
5
|
+
} | {
|
|
6
|
+
valid: false;
|
|
7
|
+
reason: 'missing' | 'mismatch' | 'stale';
|
|
8
|
+
};
|
|
9
|
+
/** Verifies `X-Communication-Signature` over `timestamp + "." + rawBody`. Pass the RAW body text, not a parsed object. During secret rotation the header carries two values; either may match. */
|
|
10
|
+
export declare function verifyEventSignature(input: {
|
|
11
|
+
secret: string | readonly string[];
|
|
12
|
+
timestamp: string | null | undefined;
|
|
13
|
+
signature: string | null | undefined;
|
|
14
|
+
rawBody: string;
|
|
15
|
+
nowSeconds?: number;
|
|
16
|
+
toleranceSeconds?: number;
|
|
17
|
+
}): Promise<VerifyResult>;
|
|
18
|
+
export interface EventHandlerOptions {
|
|
19
|
+
secret: string | readonly string[];
|
|
20
|
+
onEvent(event: MessageStateChangedEvent): void | Promise<void>;
|
|
21
|
+
/** Return true when this event, or a later one for the same message, was already processed. Keeps retries and reordering harmless. */
|
|
22
|
+
alreadyProcessed?(event: MessageStateChangedEvent): boolean | Promise<boolean>;
|
|
23
|
+
toleranceSeconds?: number;
|
|
24
|
+
}
|
|
25
|
+
/** A ready-made handler for a Next.js route or any Request/Response runtime. Answers 2xx only after `onEvent` succeeded. */
|
|
26
|
+
export declare function createEventHandler(options: EventHandlerOptions): (req: Request) => Promise<Response>;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** A random key for a send you will not retry yourself. Prefer `deriveIdempotencyKey` so your own retries are safe too. */
|
|
2
|
+
export declare function newIdempotencyKey(): string;
|
|
3
|
+
/** A stable key derived from parts you choose, for example an order id and an event name. Same input, same key. */
|
|
4
|
+
export declare function deriveIdempotencyKey(...parts: Array<string | number>): Promise<string>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type ClientOptions, type CommunicationClient } from '../client.js';
|
|
2
|
+
import type { InboxMarkReadResult, InboxNotificationPage, InboxPreference, InboxReadAllResult, InboxSummary } from '../contract.js';
|
|
3
|
+
export interface CallOptions {
|
|
4
|
+
/** Reads are retried by the SDK's policy unless this is `false`. Writes make one attempt unless this is `true`. */
|
|
5
|
+
retry?: boolean;
|
|
6
|
+
/** Bounds the whole call, in milliseconds. */
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The in-app inbox as this key sees it: what its own sub-tenant (or the tenant level, for a key outside any) sent to
|
|
11
|
+
* the person, and the tenant's announcements. Another app's notifications are not part of it.
|
|
12
|
+
*/
|
|
13
|
+
export interface InboxClient {
|
|
14
|
+
summary(personId: string, options?: CallOptions): Promise<InboxSummary>;
|
|
15
|
+
list(personId: string, query?: {
|
|
16
|
+
limit?: number;
|
|
17
|
+
cursor?: string;
|
|
18
|
+
unreadOnly?: boolean;
|
|
19
|
+
}, options?: CallOptions): Promise<InboxNotificationPage>;
|
|
20
|
+
markRead(personId: string, id: string, options?: CallOptions): Promise<InboxMarkReadResult>;
|
|
21
|
+
markAllRead(personId: string, input?: {
|
|
22
|
+
upTo?: string;
|
|
23
|
+
}, options?: CallOptions): Promise<InboxReadAllResult>;
|
|
24
|
+
/** The person's choice for this app's notifications. Another app's choice is its own. */
|
|
25
|
+
getPreference(personId: string, options?: CallOptions): Promise<InboxPreference>;
|
|
26
|
+
setPreference(personId: string, state: 'opted_in' | 'opted_out', options?: CallOptions): Promise<InboxPreference>;
|
|
27
|
+
}
|
|
28
|
+
/** `X-Communication-Person` is base64url of the UTF-8 person id, so any identifier travels safely in a header. */
|
|
29
|
+
export declare function personHeader(personId: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* The in-app inbox of one person. Pass an existing client to share its key and settings, or the same options as
|
|
32
|
+
* `createCommunicationClient`. The key needs the scopes `inbox:read` and `inbox:write`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createInboxClient(client?: ClientOptions | CommunicationClient): InboxClient;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { InboxHostErrorCode } from '../contract.js';
|
|
2
|
+
/** The six answers a browser can get from the host's inbox routes. The messages are fixed: nothing from upstream is repeated. */
|
|
3
|
+
export declare const HOST_ERRORS: Readonly<Record<InboxHostErrorCode, {
|
|
4
|
+
status: number;
|
|
5
|
+
message: string;
|
|
6
|
+
}>>;
|
|
7
|
+
/**
|
|
8
|
+
* Total: every failure is one of the six codes. A revoked or misconfigured key, a timeout and anything unknown are
|
|
9
|
+
* `unavailable`, because the person in the browser cannot fix them and must not be told about them.
|
|
10
|
+
*/
|
|
11
|
+
export declare function hostCodeOf(error: unknown): InboxHostErrorCode;
|
|
12
|
+
export declare function hostError(code: InboxHostErrorCode, field?: string): Response;
|
|
13
|
+
export declare function hostData(data: unknown): Response;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type InboxClient } from './client.js';
|
|
2
|
+
export interface InboxHandlerOptions {
|
|
3
|
+
/** Default: a client made on the first request from `COMMUNICATION_API_KEY` and `COMMUNICATION_URL`. */
|
|
4
|
+
client?: InboxClient;
|
|
5
|
+
/** The xApp's only job: who is signed in. `null` means nobody. Read it from your own session, never from the request's path, query or body. */
|
|
6
|
+
resolvePerson: (request: Request) => Promise<string | null> | string | null;
|
|
7
|
+
/** Where the routes are mounted. Default `/api/communication/inbox`. */
|
|
8
|
+
basePath?: string;
|
|
9
|
+
/** Origins that may send POST and PUT. Default: the request's own origin. */
|
|
10
|
+
allowedOrigins?: readonly string[];
|
|
11
|
+
/** Default 8000. */
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
/** Told about every failure answered as `unavailable`, for the host's own log. The browser never sees it. */
|
|
14
|
+
onError?: (error: unknown) => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The host xApp's browser-facing inbox routes, in one tested function. Mount it on a catch-all route:
|
|
18
|
+
*
|
|
19
|
+
* const handler = createInboxHandler({ resolvePerson });
|
|
20
|
+
* export { handler as GET, handler as POST, handler as PUT };
|
|
21
|
+
*
|
|
22
|
+
* The person comes only from `resolvePerson`. The host stores nothing. Without `client`, the key and the address are
|
|
23
|
+
* read on the first request, so a missing one answers `unavailable` and never stops the host's build.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createInboxHandler(options: InboxHandlerOptions): (request: Request) => Promise<Response>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createInboxClient, personHeader, type CallOptions, type InboxClient } from './client.js';
|
|
2
|
+
export { createInboxHandler, type InboxHandlerOptions } from './handler.js';
|
|
3
|
+
export { HOST_ERRORS, hostCodeOf } from './errors.js';
|
|
4
|
+
export { originAllowed } from './origin.js';
|
|
5
|
+
export { INBOX_LIMITS } from './validate.js';
|
|
6
|
+
export type { InboxHostErrorBody, InboxHostErrorCode, InboxMarkReadResult, InboxNotification, InboxNotificationPage, InboxPreference, InboxReadAllResult, InboxSummary } from '../contract.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A request that changes something must come from the host's own pages (14.5): the browser says `Sec-Fetch-Site: same-origin`,
|
|
3
|
+
* or the `Origin` header equals an allowed origin. Without `allowedOrigins`, the only allowed origin is the request's own.
|
|
4
|
+
*/
|
|
5
|
+
export declare function originAllowed(request: Request, allowedOrigins?: readonly string[]): boolean;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { InboxMarkReadResult, InboxNotification, InboxNotificationPage, InboxPreference, InboxReadAllResult, InboxSummary } from '../contract.js';
|
|
2
|
+
export declare function projectNotification(v: unknown): InboxNotification | null;
|
|
3
|
+
export declare function projectSummary(v: unknown): InboxSummary | null;
|
|
4
|
+
export declare function projectPage(v: unknown): InboxNotificationPage | null;
|
|
5
|
+
export declare function projectMarkRead(v: unknown): InboxMarkReadResult | null;
|
|
6
|
+
export declare function projectReadAll(v: unknown): InboxReadAllResult | null;
|
|
7
|
+
export declare function projectPreference(v: unknown): InboxPreference | null;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Bounded parsing of every value that comes from a browser. A value is either refused, naming its field, or inside the documented bounds. */
|
|
2
|
+
export type Parsed<T> = {
|
|
3
|
+
ok: true;
|
|
4
|
+
value: T;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
field: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const INBOX_LIMITS: {
|
|
10
|
+
readonly pageMin: 1;
|
|
11
|
+
readonly pageMax: 100;
|
|
12
|
+
readonly cursorMax: 128;
|
|
13
|
+
readonly idMax: 64;
|
|
14
|
+
readonly upToMax: 40;
|
|
15
|
+
readonly bodyBytes: 1024;
|
|
16
|
+
readonly personMax: 128;
|
|
17
|
+
};
|
|
18
|
+
export declare function parseLimit(raw: string | null): Parsed<number | undefined>;
|
|
19
|
+
export declare function parseCursor(raw: string | null): Parsed<string | undefined>;
|
|
20
|
+
export declare function parseUnread(raw: string | null): Parsed<boolean>;
|
|
21
|
+
export declare function parseId(raw: string): Parsed<string>;
|
|
22
|
+
export declare function parseUpTo(raw: unknown): Parsed<string | undefined>;
|
|
23
|
+
export declare function parseState(raw: unknown): Parsed<'opted_in' | 'opted_out'>;
|
|
24
|
+
/** A JSON object of at most 1024 bytes. An empty body is an empty object. */
|
|
25
|
+
export declare function parseBody(text: string): Parsed<Record<string, unknown>>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { InboxLabels } from './labels.js';
|
|
2
|
+
/** Fills `{name}` placeholders. Values are text, never markup. */
|
|
3
|
+
export declare function fill(template: string, values: Record<string, string | number>): string;
|
|
4
|
+
/** Nothing for 0, the number for 1 to 99, "99+" above (12.1). */
|
|
5
|
+
export declare function badgeText(count: number): string;
|
|
6
|
+
/** The bell's accessible name: "{label}, {n} unread", or the label alone when nothing is unread (15.3). */
|
|
7
|
+
export declare function bellName(labels: Pick<InboxLabels, 'bell' | 'bellWithCount'>, count: number): string;
|
|
8
|
+
/** Accepts exactly what the inbox stores: an absolute https URL without credentials, at most 2048 characters (12.15). */
|
|
9
|
+
export declare function safeLink(link: string | null | undefined): string | null;
|
|
10
|
+
/** "now", "5 minutes ago", "yesterday", then a short date after a week. A time in the future (clock skew) reads as now. */
|
|
11
|
+
export declare function relativeTime(iso: string, now: Date, locale?: string): string;
|
|
12
|
+
/** The full date and time, for the `title` of a relative time. */
|
|
13
|
+
export declare function exactTime(iso: string, locale?: string): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { InboxMarkReadResult, InboxNotificationPage, InboxPreference, InboxReadAllResult, InboxResult, InboxSummary } from './types.js';
|
|
2
|
+
export declare const DEFAULT_API_BASE = "/api/communication/inbox";
|
|
3
|
+
/**
|
|
4
|
+
* The browser talks only to the host xApp's own origin (14.2). The base is a path on that origin: an absolute URL,
|
|
5
|
+
* a protocol-relative one or anything with a query is refused.
|
|
6
|
+
*/
|
|
7
|
+
export declare function normalizeApiBase(apiBase: string | undefined): string | null;
|
|
8
|
+
export interface InboxBrowserClient {
|
|
9
|
+
summary(): Promise<InboxResult<InboxSummary>>;
|
|
10
|
+
list(query?: {
|
|
11
|
+
cursor?: string | null;
|
|
12
|
+
unreadOnly?: boolean;
|
|
13
|
+
limit?: number;
|
|
14
|
+
}): Promise<InboxResult<InboxNotificationPage>>;
|
|
15
|
+
markRead(id: string): Promise<InboxResult<InboxMarkReadResult>>;
|
|
16
|
+
markAllRead(upTo?: string | null): Promise<InboxResult<InboxReadAllResult>>;
|
|
17
|
+
getPreference(): Promise<InboxResult<InboxPreference>>;
|
|
18
|
+
setPreference(state: 'opted_in' | 'opted_out'): Promise<InboxResult<InboxPreference>>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Every call answers a result and never throws, so an outage cannot break the host page (14.9).
|
|
22
|
+
* The bell shows what this xApp's own sub-tenant sent the person, and the tenant's announcements: there is nothing to choose.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createInboxBrowserClient(apiBase?: string, fetchImpl?: typeof fetch): InboxBrowserClient;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
import type { InboxBrowserClient } from './inbox-client.js';
|
|
3
|
+
import type { InboxState } from './inbox-state.js';
|
|
4
|
+
import type { InboxLabels } from './labels.js';
|
|
5
|
+
import type { InboxNotification, InboxTab } from './types.js';
|
|
6
|
+
export interface InboxContextValue {
|
|
7
|
+
state: InboxState;
|
|
8
|
+
labels: InboxLabels;
|
|
9
|
+
locale: string | undefined;
|
|
10
|
+
client: InboxBrowserClient;
|
|
11
|
+
panelOpened: boolean;
|
|
12
|
+
/** The bell, so focus returns to it when the panel or a dialog closes. */
|
|
13
|
+
bellRef: RefObject<HTMLButtonElement | null>;
|
|
14
|
+
openPanel: () => void;
|
|
15
|
+
closePanel: () => void;
|
|
16
|
+
setTab: (tab: InboxTab) => void;
|
|
17
|
+
/** Marks the notification read and opens its link when it has a safe one. */
|
|
18
|
+
select: (item: InboxNotification) => void;
|
|
19
|
+
markAllRead: () => void;
|
|
20
|
+
loadMore: () => void;
|
|
21
|
+
viewAll: () => void;
|
|
22
|
+
openPreferences: () => void;
|
|
23
|
+
retry: () => void;
|
|
24
|
+
clearMessage: () => void;
|
|
25
|
+
/** A visible list calls this on mount and the returned function on unmount; a shown list is kept fresh by the poll. */
|
|
26
|
+
attachList: () => () => void;
|
|
27
|
+
}
|
|
28
|
+
export declare const InboxContext: import("react").Context<InboxContextValue | null>;
|
|
29
|
+
export declare function useInbox(): InboxContextValue;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every notification, a page at a time. The provider shows it in a dialog for "View all"; a host that passes
|
|
3
|
+
* `onViewAll` can place it on a page of its own instead.
|
|
4
|
+
*/
|
|
5
|
+
export declare function InboxFullList({ maxHeight }: {
|
|
6
|
+
maxHeight?: number | string;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
interface InboxListViewProps {
|
|
3
|
+
/** Shown beside "Mark all as read". */
|
|
4
|
+
heading?: ReactNode;
|
|
5
|
+
/** The list scrolls inside this height. */
|
|
6
|
+
maxHeight?: number | string;
|
|
7
|
+
/** Offers older notifications below the list. */
|
|
8
|
+
paged?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** Tabs, "Mark all as read", the list and its loading, empty and error states. Shared by the panel and the full list. */
|
|
11
|
+
export declare function InboxListView({ heading, maxHeight, paged }: InboxListViewProps): import("react").JSX.Element;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type InboxLabels } from './labels.js';
|
|
3
|
+
export interface InboxProviderProps {
|
|
4
|
+
/** A path on the host's own origin. Default `/api/communication/inbox`. */
|
|
5
|
+
apiBase?: string;
|
|
6
|
+
/** Replaces any visible or accessible text. */
|
|
7
|
+
labels?: Partial<InboxLabels>;
|
|
8
|
+
/** Formats relative and exact times. Default: the browser's locale. */
|
|
9
|
+
locale?: string;
|
|
10
|
+
/** Default: opens the built-in full list in a dialog. */
|
|
11
|
+
onViewAll?: () => void;
|
|
12
|
+
/** Default: opens the built-in preference dialog. */
|
|
13
|
+
onOpenPreferences?: () => void;
|
|
14
|
+
/** Default: a new tab with `noopener,noreferrer`. */
|
|
15
|
+
openLink?: (url: string) => void;
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
export declare function InboxProvider({ apiBase, labels, locale, onViewAll, onOpenPreferences, openLink, children }: InboxProviderProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { InboxNotification, InboxNotificationPage, InboxSummary, InboxTab } from './types.js';
|
|
2
|
+
export interface InboxState {
|
|
3
|
+
unreadCount: number;
|
|
4
|
+
newestUnreadAt: string | null;
|
|
5
|
+
items: InboxNotification[];
|
|
6
|
+
nextCursor: string | null;
|
|
7
|
+
tab: InboxTab;
|
|
8
|
+
/** `error` only when nothing could be shown; a failed refresh keeps the last data. */
|
|
9
|
+
status: 'idle' | 'loading' | 'error' | 'refreshing';
|
|
10
|
+
/** The current tab has been loaded at least once. */
|
|
11
|
+
loaded: boolean;
|
|
12
|
+
loadingMore: boolean;
|
|
13
|
+
lastSyncedAt: string | null;
|
|
14
|
+
/** Items marked read before the server answered, as they were, so a failure puts them back. */
|
|
15
|
+
pending: Record<string, InboxNotification>;
|
|
16
|
+
/** The list and count as they were before "Mark all as read", until the server answers. */
|
|
17
|
+
markAll: {
|
|
18
|
+
items: InboxNotification[];
|
|
19
|
+
unreadCount: number;
|
|
20
|
+
} | null;
|
|
21
|
+
/** An action failed and was undone. Shown without closing the panel. */
|
|
22
|
+
actionFailed: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare const initialInboxState: InboxState;
|
|
25
|
+
/** `replace` shows a first page; `append` adds an older page; `merge` refreshes the newest page and keeps older ones already shown. */
|
|
26
|
+
export type PageMode = 'replace' | 'append' | 'merge';
|
|
27
|
+
export type InboxAction = {
|
|
28
|
+
type: 'summaryReceived';
|
|
29
|
+
summary: InboxSummary;
|
|
30
|
+
at: string;
|
|
31
|
+
} | {
|
|
32
|
+
type: 'loadStarted';
|
|
33
|
+
mode: PageMode;
|
|
34
|
+
} | {
|
|
35
|
+
type: 'pageReceived';
|
|
36
|
+
page: InboxNotificationPage;
|
|
37
|
+
mode: PageMode;
|
|
38
|
+
at: string;
|
|
39
|
+
} | {
|
|
40
|
+
type: 'loadFailed';
|
|
41
|
+
mode: PageMode;
|
|
42
|
+
} | {
|
|
43
|
+
type: 'tabChanged';
|
|
44
|
+
tab: InboxTab;
|
|
45
|
+
} | {
|
|
46
|
+
type: 'markReadOptimistic';
|
|
47
|
+
id: string;
|
|
48
|
+
at: string;
|
|
49
|
+
} | {
|
|
50
|
+
type: 'markReadConfirmed';
|
|
51
|
+
id: string;
|
|
52
|
+
notification: InboxNotification;
|
|
53
|
+
unreadCount: number;
|
|
54
|
+
} | {
|
|
55
|
+
type: 'markReadFailed';
|
|
56
|
+
id: string;
|
|
57
|
+
} | {
|
|
58
|
+
type: 'markAllOptimistic';
|
|
59
|
+
at: string;
|
|
60
|
+
} | {
|
|
61
|
+
type: 'markAllConfirmed';
|
|
62
|
+
unreadCount: number;
|
|
63
|
+
} | {
|
|
64
|
+
type: 'markAllFailed';
|
|
65
|
+
} | {
|
|
66
|
+
type: 'messageCleared';
|
|
67
|
+
};
|
|
68
|
+
export declare function inboxReducer(s: InboxState, a: InboxAction): InboxState;
|
|
69
|
+
/** "Mark all as read" is offered only when something is unread and no such request is running (12.8). */
|
|
70
|
+
export declare function canMarkAll(s: InboxState): boolean;
|