nofax 0.2.0 → 0.2.1
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/LICENSE +21 -21
- package/README.md +349 -351
- package/SECURITY.md +144 -144
- package/bin/nofax.mjs +4 -4
- package/package.json +56 -56
- package/src/adapters/claude.mjs +47 -47
- package/src/adapters/codex.mjs +47 -47
- package/src/adapters/gemini.mjs +57 -57
- package/src/cli.mjs +191 -191
- package/src/config.mjs +91 -91
- package/src/index.mjs +8 -8
- package/src/mcp-server.mjs +141 -141
- package/src/mcp-tools.mjs +240 -240
- package/src/ntfy.mjs +259 -259
- package/src/protocol.mjs +108 -108
- package/src/requests.mjs +120 -120
package/src/ntfy.mjs
CHANGED
|
@@ -1,259 +1,259 @@
|
|
|
1
|
-
import { createRequestId, createResponseTopic, parseResponseMessage } from './protocol.mjs';
|
|
2
|
-
|
|
3
|
-
const MAX_TITLE = 120;
|
|
4
|
-
const MAX_MESSAGE = 2200;
|
|
5
|
-
const DEFAULT_REFINE_SHORTCUT = 'Nofax Refine';
|
|
6
|
-
|
|
7
|
-
function boundText(value, max, name) {
|
|
8
|
-
if (typeof value !== 'string' || value.trim().length === 0) throw new Error(`NOFAX_${name}_REQUIRED`);
|
|
9
|
-
const text = value.trim();
|
|
10
|
-
return text.length <= max ? text : `${text.slice(0, max - 14)}…[truncated]`;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function sleep(ms) {
|
|
14
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function httpAction({ label, server, responseTopic, requestId, decision }) {
|
|
18
|
-
return {
|
|
19
|
-
action: 'http',
|
|
20
|
-
label,
|
|
21
|
-
url: `${server}/${responseTopic}`,
|
|
22
|
-
method: 'POST',
|
|
23
|
-
body: JSON.stringify({ v: 1, requestId, decision }),
|
|
24
|
-
clear: true
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function buildRefineShortcutUrl({ shortcutName = DEFAULT_REFINE_SHORTCUT, server, responseTopic, requestId }) {
|
|
29
|
-
const payload = JSON.stringify({
|
|
30
|
-
v: 1,
|
|
31
|
-
requestId,
|
|
32
|
-
callbackUrl: `${server}/${responseTopic}`
|
|
33
|
-
});
|
|
34
|
-
const url = new URL('shortcuts://run-shortcut');
|
|
35
|
-
url.searchParams.set('name', shortcutName);
|
|
36
|
-
url.searchParams.set('input', 'text');
|
|
37
|
-
url.searchParams.set('text', payload);
|
|
38
|
-
return url.toString();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function refineAction({ shortcutName, server, responseTopic, requestId }) {
|
|
42
|
-
return {
|
|
43
|
-
action: 'view',
|
|
44
|
-
label: 'Refine',
|
|
45
|
-
url: buildRefineShortcutUrl({ shortcutName, server, responseTopic, requestId }),
|
|
46
|
-
clear: true
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function ensureOk(response, code) {
|
|
51
|
-
if (!response?.ok) throw new Error(`${code}_${response?.status ?? 'NETWORK'}`);
|
|
52
|
-
return response;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export async function sendNotification({ config, title, message, actions, priority = 4, tags = ['bell'], fetchImpl = fetch }) {
|
|
56
|
-
const payload = {
|
|
57
|
-
topic: config.topic,
|
|
58
|
-
title: boundText(title, MAX_TITLE, 'TITLE'),
|
|
59
|
-
message: boundText(message, MAX_MESSAGE, 'MESSAGE'),
|
|
60
|
-
priority,
|
|
61
|
-
tags
|
|
62
|
-
};
|
|
63
|
-
if (actions?.length) payload.actions = actions;
|
|
64
|
-
|
|
65
|
-
let response;
|
|
66
|
-
try {
|
|
67
|
-
response = await fetchImpl(`${config.server}/`, {
|
|
68
|
-
method: 'POST',
|
|
69
|
-
headers: { 'content-type': 'application/json' },
|
|
70
|
-
body: JSON.stringify(payload)
|
|
71
|
-
});
|
|
72
|
-
} catch (error) {
|
|
73
|
-
throw new Error(`NOFAX_NTFY_PUBLISH_NETWORK: ${error?.message ?? String(error)}`);
|
|
74
|
-
}
|
|
75
|
-
await ensureOk(response, 'NOFAX_NTFY_PUBLISH');
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function normalizeOptions(options) {
|
|
79
|
-
if (!Array.isArray(options) || options.length > 3) throw new Error('NOFAX_CHOICE_LIMIT');
|
|
80
|
-
const normalized = options.map((option) => {
|
|
81
|
-
if (typeof option === 'string') return { value: option, label: option };
|
|
82
|
-
if (!option || typeof option.value !== 'string' || typeof option.label !== 'string') throw new Error('NOFAX_CHOICE_INVALID');
|
|
83
|
-
const value = option.value.trim();
|
|
84
|
-
const label = option.label.trim();
|
|
85
|
-
if (!value || !label || value.length > 80) throw new Error('NOFAX_CHOICE_INVALID');
|
|
86
|
-
return { value, label: label.slice(0, 32) };
|
|
87
|
-
});
|
|
88
|
-
if (new Set(normalized.map((option) => option.value)).size !== normalized.length) throw new Error('NOFAX_CHOICE_DUPLICATE');
|
|
89
|
-
return normalized;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export async function createRemoteRequest({
|
|
93
|
-
config,
|
|
94
|
-
title,
|
|
95
|
-
message,
|
|
96
|
-
options = [],
|
|
97
|
-
includeRefine = false,
|
|
98
|
-
shortcutName = config.refineShortcutName ?? DEFAULT_REFINE_SHORTCUT,
|
|
99
|
-
fetchImpl = fetch
|
|
100
|
-
}) {
|
|
101
|
-
const normalized = normalizeOptions(options);
|
|
102
|
-
const totalActions = normalized.length + (includeRefine ? 1 : 0);
|
|
103
|
-
if (totalActions < 1 || totalActions > 3) throw new Error('NOFAX_CHOICE_LIMIT');
|
|
104
|
-
|
|
105
|
-
const requestId = createRequestId();
|
|
106
|
-
const responseTopic = createResponseTopic();
|
|
107
|
-
const actions = [];
|
|
108
|
-
const allowed = [];
|
|
109
|
-
|
|
110
|
-
if (includeRefine && normalized.length === 2) {
|
|
111
|
-
const [first, second] = normalized;
|
|
112
|
-
actions.push(httpAction({ label: first.label, server: config.server, responseTopic, requestId, decision: first.value }));
|
|
113
|
-
allowed.push(first.value);
|
|
114
|
-
actions.push(refineAction({ shortcutName, server: config.server, responseTopic, requestId }));
|
|
115
|
-
allowed.push('refine');
|
|
116
|
-
actions.push(httpAction({ label: second.label, server: config.server, responseTopic, requestId, decision: second.value }));
|
|
117
|
-
allowed.push(second.value);
|
|
118
|
-
} else {
|
|
119
|
-
for (const option of normalized) {
|
|
120
|
-
actions.push(httpAction({ label: option.label, server: config.server, responseTopic, requestId, decision: option.value }));
|
|
121
|
-
allowed.push(option.value);
|
|
122
|
-
}
|
|
123
|
-
if (includeRefine) {
|
|
124
|
-
actions.push(refineAction({ shortcutName, server: config.server, responseTopic, requestId }));
|
|
125
|
-
allowed.push('refine');
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
await sendNotification({ config, title, message, actions, fetchImpl });
|
|
130
|
-
return { requestId, responseTopic, allowed };
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function parseNtfyPoll(text, requestId, allowed) {
|
|
134
|
-
for (const line of text.split(/\r?\n/)) {
|
|
135
|
-
if (!line.trim()) continue;
|
|
136
|
-
let event;
|
|
137
|
-
try {
|
|
138
|
-
event = JSON.parse(line);
|
|
139
|
-
} catch {
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
if (event?.event !== 'message') continue;
|
|
143
|
-
const response = parseResponseMessage(event.message, { requestId, allowed });
|
|
144
|
-
if (response !== null) return response;
|
|
145
|
-
}
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export async function pollRemoteResponse({ config, responseTopic, requestId, allowed, fetchImpl = fetch }) {
|
|
150
|
-
let response;
|
|
151
|
-
try {
|
|
152
|
-
response = await fetchImpl(`${config.server}/${responseTopic}/json?poll=1&since=10m`, {
|
|
153
|
-
method: 'GET',
|
|
154
|
-
headers: { accept: 'application/x-ndjson' }
|
|
155
|
-
});
|
|
156
|
-
} catch (error) {
|
|
157
|
-
throw new Error(`NOFAX_NTFY_POLL_NETWORK: ${error?.message ?? String(error)}`);
|
|
158
|
-
}
|
|
159
|
-
await ensureOk(response, 'NOFAX_NTFY_POLL');
|
|
160
|
-
return parseNtfyPoll(await response.text(), requestId, allowed);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export async function sendResponseConfirmation({ config, response, title = 'Nofax', fetchImpl = fetch }) {
|
|
164
|
-
let confirmationTitle = 'Response received';
|
|
165
|
-
let tag = 'white_check_mark';
|
|
166
|
-
if (response.decision === 'allow') confirmationTitle = 'Approved';
|
|
167
|
-
else if (response.decision === 'deny') {
|
|
168
|
-
confirmationTitle = 'Denied';
|
|
169
|
-
tag = 'no_entry';
|
|
170
|
-
} else if (response.decision === 'refine') confirmationTitle = 'Refinement received';
|
|
171
|
-
else confirmationTitle = 'Choice received';
|
|
172
|
-
|
|
173
|
-
try {
|
|
174
|
-
await sendNotification({
|
|
175
|
-
config,
|
|
176
|
-
title: `${confirmationTitle} - ${title}`,
|
|
177
|
-
message: response.decision === 'refine'
|
|
178
|
-
? 'Your refinement was sent back to the agent.'
|
|
179
|
-
: `Nofax recorded: ${response.decision}`,
|
|
180
|
-
priority: 2,
|
|
181
|
-
tags: [tag],
|
|
182
|
-
fetchImpl
|
|
183
|
-
});
|
|
184
|
-
return true;
|
|
185
|
-
} catch {
|
|
186
|
-
return false;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export async function waitRemoteResponse({
|
|
191
|
-
config,
|
|
192
|
-
responseTopic,
|
|
193
|
-
requestId,
|
|
194
|
-
allowed,
|
|
195
|
-
timeoutMs,
|
|
196
|
-
pollIntervalMs = 1000,
|
|
197
|
-
fetchImpl = fetch,
|
|
198
|
-
nowImpl = Date.now,
|
|
199
|
-
sleepImpl = sleep
|
|
200
|
-
}) {
|
|
201
|
-
const deadline = nowImpl() + timeoutMs;
|
|
202
|
-
while (nowImpl() < deadline) {
|
|
203
|
-
const response = await pollRemoteResponse({ config, responseTopic, requestId, allowed, fetchImpl });
|
|
204
|
-
if (response !== null) return response;
|
|
205
|
-
const remaining = deadline - nowImpl();
|
|
206
|
-
if (remaining <= 0) break;
|
|
207
|
-
await sleepImpl(Math.min(pollIntervalMs, remaining));
|
|
208
|
-
}
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async function requestDecision({
|
|
213
|
-
config,
|
|
214
|
-
title,
|
|
215
|
-
message,
|
|
216
|
-
options,
|
|
217
|
-
includeRefine = false,
|
|
218
|
-
fetchImpl = fetch,
|
|
219
|
-
timeoutMs = config.timeoutSeconds * 1000,
|
|
220
|
-
pollIntervalMs = 1000,
|
|
221
|
-
nowImpl = Date.now,
|
|
222
|
-
sleepImpl = sleep
|
|
223
|
-
}) {
|
|
224
|
-
const remote = await createRemoteRequest({ config, title, message, options, includeRefine, fetchImpl });
|
|
225
|
-
const response = await waitRemoteResponse({
|
|
226
|
-
config,
|
|
227
|
-
...remote,
|
|
228
|
-
timeoutMs,
|
|
229
|
-
pollIntervalMs,
|
|
230
|
-
fetchImpl,
|
|
231
|
-
nowImpl,
|
|
232
|
-
sleepImpl
|
|
233
|
-
});
|
|
234
|
-
if (response === null) {
|
|
235
|
-
return { decision: 'timeout', requestId: remote.requestId, responseTopic: remote.responseTopic };
|
|
236
|
-
}
|
|
237
|
-
await sendResponseConfirmation({ config, response, title, fetchImpl });
|
|
238
|
-
return { ...response, requestId: remote.requestId, responseTopic: remote.responseTopic };
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
export async function requestApproval(options) {
|
|
242
|
-
return requestDecision({
|
|
243
|
-
...options,
|
|
244
|
-
options: [
|
|
245
|
-
{ value: 'allow', label: 'Allow' },
|
|
246
|
-
{ value: 'deny', label: 'Deny' }
|
|
247
|
-
]
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
export async function requestRefinement(options) {
|
|
252
|
-
return requestDecision({ ...options, options: [], includeRefine: true });
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
export async function requestChoice({ options, ...rest }) {
|
|
256
|
-
const normalized = normalizeOptions(options);
|
|
257
|
-
if (normalized.length < 1 || normalized.length > 3) throw new Error('NOFAX_CHOICE_LIMIT');
|
|
258
|
-
return requestDecision({ ...rest, options: normalized });
|
|
259
|
-
}
|
|
1
|
+
import { createRequestId, createResponseTopic, parseResponseMessage } from './protocol.mjs';
|
|
2
|
+
|
|
3
|
+
const MAX_TITLE = 120;
|
|
4
|
+
const MAX_MESSAGE = 2200;
|
|
5
|
+
const DEFAULT_REFINE_SHORTCUT = 'Nofax Refine';
|
|
6
|
+
|
|
7
|
+
function boundText(value, max, name) {
|
|
8
|
+
if (typeof value !== 'string' || value.trim().length === 0) throw new Error(`NOFAX_${name}_REQUIRED`);
|
|
9
|
+
const text = value.trim();
|
|
10
|
+
return text.length <= max ? text : `${text.slice(0, max - 14)}…[truncated]`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function sleep(ms) {
|
|
14
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function httpAction({ label, server, responseTopic, requestId, decision }) {
|
|
18
|
+
return {
|
|
19
|
+
action: 'http',
|
|
20
|
+
label,
|
|
21
|
+
url: `${server}/${responseTopic}`,
|
|
22
|
+
method: 'POST',
|
|
23
|
+
body: JSON.stringify({ v: 1, requestId, decision }),
|
|
24
|
+
clear: true
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildRefineShortcutUrl({ shortcutName = DEFAULT_REFINE_SHORTCUT, server, responseTopic, requestId }) {
|
|
29
|
+
const payload = JSON.stringify({
|
|
30
|
+
v: 1,
|
|
31
|
+
requestId,
|
|
32
|
+
callbackUrl: `${server}/${responseTopic}`
|
|
33
|
+
});
|
|
34
|
+
const url = new URL('shortcuts://run-shortcut');
|
|
35
|
+
url.searchParams.set('name', shortcutName);
|
|
36
|
+
url.searchParams.set('input', 'text');
|
|
37
|
+
url.searchParams.set('text', payload);
|
|
38
|
+
return url.toString();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function refineAction({ shortcutName, server, responseTopic, requestId }) {
|
|
42
|
+
return {
|
|
43
|
+
action: 'view',
|
|
44
|
+
label: 'Refine',
|
|
45
|
+
url: buildRefineShortcutUrl({ shortcutName, server, responseTopic, requestId }),
|
|
46
|
+
clear: true
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function ensureOk(response, code) {
|
|
51
|
+
if (!response?.ok) throw new Error(`${code}_${response?.status ?? 'NETWORK'}`);
|
|
52
|
+
return response;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function sendNotification({ config, title, message, actions, priority = 4, tags = ['bell'], fetchImpl = fetch }) {
|
|
56
|
+
const payload = {
|
|
57
|
+
topic: config.topic,
|
|
58
|
+
title: boundText(title, MAX_TITLE, 'TITLE'),
|
|
59
|
+
message: boundText(message, MAX_MESSAGE, 'MESSAGE'),
|
|
60
|
+
priority,
|
|
61
|
+
tags
|
|
62
|
+
};
|
|
63
|
+
if (actions?.length) payload.actions = actions;
|
|
64
|
+
|
|
65
|
+
let response;
|
|
66
|
+
try {
|
|
67
|
+
response = await fetchImpl(`${config.server}/`, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'content-type': 'application/json' },
|
|
70
|
+
body: JSON.stringify(payload)
|
|
71
|
+
});
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw new Error(`NOFAX_NTFY_PUBLISH_NETWORK: ${error?.message ?? String(error)}`);
|
|
74
|
+
}
|
|
75
|
+
await ensureOk(response, 'NOFAX_NTFY_PUBLISH');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeOptions(options) {
|
|
79
|
+
if (!Array.isArray(options) || options.length > 3) throw new Error('NOFAX_CHOICE_LIMIT');
|
|
80
|
+
const normalized = options.map((option) => {
|
|
81
|
+
if (typeof option === 'string') return { value: option, label: option };
|
|
82
|
+
if (!option || typeof option.value !== 'string' || typeof option.label !== 'string') throw new Error('NOFAX_CHOICE_INVALID');
|
|
83
|
+
const value = option.value.trim();
|
|
84
|
+
const label = option.label.trim();
|
|
85
|
+
if (!value || !label || value.length > 80) throw new Error('NOFAX_CHOICE_INVALID');
|
|
86
|
+
return { value, label: label.slice(0, 32) };
|
|
87
|
+
});
|
|
88
|
+
if (new Set(normalized.map((option) => option.value)).size !== normalized.length) throw new Error('NOFAX_CHOICE_DUPLICATE');
|
|
89
|
+
return normalized;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function createRemoteRequest({
|
|
93
|
+
config,
|
|
94
|
+
title,
|
|
95
|
+
message,
|
|
96
|
+
options = [],
|
|
97
|
+
includeRefine = false,
|
|
98
|
+
shortcutName = config.refineShortcutName ?? DEFAULT_REFINE_SHORTCUT,
|
|
99
|
+
fetchImpl = fetch
|
|
100
|
+
}) {
|
|
101
|
+
const normalized = normalizeOptions(options);
|
|
102
|
+
const totalActions = normalized.length + (includeRefine ? 1 : 0);
|
|
103
|
+
if (totalActions < 1 || totalActions > 3) throw new Error('NOFAX_CHOICE_LIMIT');
|
|
104
|
+
|
|
105
|
+
const requestId = createRequestId();
|
|
106
|
+
const responseTopic = createResponseTopic();
|
|
107
|
+
const actions = [];
|
|
108
|
+
const allowed = [];
|
|
109
|
+
|
|
110
|
+
if (includeRefine && normalized.length === 2) {
|
|
111
|
+
const [first, second] = normalized;
|
|
112
|
+
actions.push(httpAction({ label: first.label, server: config.server, responseTopic, requestId, decision: first.value }));
|
|
113
|
+
allowed.push(first.value);
|
|
114
|
+
actions.push(refineAction({ shortcutName, server: config.server, responseTopic, requestId }));
|
|
115
|
+
allowed.push('refine');
|
|
116
|
+
actions.push(httpAction({ label: second.label, server: config.server, responseTopic, requestId, decision: second.value }));
|
|
117
|
+
allowed.push(second.value);
|
|
118
|
+
} else {
|
|
119
|
+
for (const option of normalized) {
|
|
120
|
+
actions.push(httpAction({ label: option.label, server: config.server, responseTopic, requestId, decision: option.value }));
|
|
121
|
+
allowed.push(option.value);
|
|
122
|
+
}
|
|
123
|
+
if (includeRefine) {
|
|
124
|
+
actions.push(refineAction({ shortcutName, server: config.server, responseTopic, requestId }));
|
|
125
|
+
allowed.push('refine');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await sendNotification({ config, title, message, actions, fetchImpl });
|
|
130
|
+
return { requestId, responseTopic, allowed };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseNtfyPoll(text, requestId, allowed) {
|
|
134
|
+
for (const line of text.split(/\r?\n/)) {
|
|
135
|
+
if (!line.trim()) continue;
|
|
136
|
+
let event;
|
|
137
|
+
try {
|
|
138
|
+
event = JSON.parse(line);
|
|
139
|
+
} catch {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (event?.event !== 'message') continue;
|
|
143
|
+
const response = parseResponseMessage(event.message, { requestId, allowed });
|
|
144
|
+
if (response !== null) return response;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function pollRemoteResponse({ config, responseTopic, requestId, allowed, fetchImpl = fetch }) {
|
|
150
|
+
let response;
|
|
151
|
+
try {
|
|
152
|
+
response = await fetchImpl(`${config.server}/${responseTopic}/json?poll=1&since=10m`, {
|
|
153
|
+
method: 'GET',
|
|
154
|
+
headers: { accept: 'application/x-ndjson' }
|
|
155
|
+
});
|
|
156
|
+
} catch (error) {
|
|
157
|
+
throw new Error(`NOFAX_NTFY_POLL_NETWORK: ${error?.message ?? String(error)}`);
|
|
158
|
+
}
|
|
159
|
+
await ensureOk(response, 'NOFAX_NTFY_POLL');
|
|
160
|
+
return parseNtfyPoll(await response.text(), requestId, allowed);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function sendResponseConfirmation({ config, response, title = 'Nofax', fetchImpl = fetch }) {
|
|
164
|
+
let confirmationTitle = 'Response received';
|
|
165
|
+
let tag = 'white_check_mark';
|
|
166
|
+
if (response.decision === 'allow') confirmationTitle = 'Approved';
|
|
167
|
+
else if (response.decision === 'deny') {
|
|
168
|
+
confirmationTitle = 'Denied';
|
|
169
|
+
tag = 'no_entry';
|
|
170
|
+
} else if (response.decision === 'refine') confirmationTitle = 'Refinement received';
|
|
171
|
+
else confirmationTitle = 'Choice received';
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
await sendNotification({
|
|
175
|
+
config,
|
|
176
|
+
title: `${confirmationTitle} - ${title}`,
|
|
177
|
+
message: response.decision === 'refine'
|
|
178
|
+
? 'Your refinement was sent back to the agent.'
|
|
179
|
+
: `Nofax recorded: ${response.decision}`,
|
|
180
|
+
priority: 2,
|
|
181
|
+
tags: [tag],
|
|
182
|
+
fetchImpl
|
|
183
|
+
});
|
|
184
|
+
return true;
|
|
185
|
+
} catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function waitRemoteResponse({
|
|
191
|
+
config,
|
|
192
|
+
responseTopic,
|
|
193
|
+
requestId,
|
|
194
|
+
allowed,
|
|
195
|
+
timeoutMs,
|
|
196
|
+
pollIntervalMs = 1000,
|
|
197
|
+
fetchImpl = fetch,
|
|
198
|
+
nowImpl = Date.now,
|
|
199
|
+
sleepImpl = sleep
|
|
200
|
+
}) {
|
|
201
|
+
const deadline = nowImpl() + timeoutMs;
|
|
202
|
+
while (nowImpl() < deadline) {
|
|
203
|
+
const response = await pollRemoteResponse({ config, responseTopic, requestId, allowed, fetchImpl });
|
|
204
|
+
if (response !== null) return response;
|
|
205
|
+
const remaining = deadline - nowImpl();
|
|
206
|
+
if (remaining <= 0) break;
|
|
207
|
+
await sleepImpl(Math.min(pollIntervalMs, remaining));
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function requestDecision({
|
|
213
|
+
config,
|
|
214
|
+
title,
|
|
215
|
+
message,
|
|
216
|
+
options,
|
|
217
|
+
includeRefine = false,
|
|
218
|
+
fetchImpl = fetch,
|
|
219
|
+
timeoutMs = config.timeoutSeconds * 1000,
|
|
220
|
+
pollIntervalMs = 1000,
|
|
221
|
+
nowImpl = Date.now,
|
|
222
|
+
sleepImpl = sleep
|
|
223
|
+
}) {
|
|
224
|
+
const remote = await createRemoteRequest({ config, title, message, options, includeRefine, fetchImpl });
|
|
225
|
+
const response = await waitRemoteResponse({
|
|
226
|
+
config,
|
|
227
|
+
...remote,
|
|
228
|
+
timeoutMs,
|
|
229
|
+
pollIntervalMs,
|
|
230
|
+
fetchImpl,
|
|
231
|
+
nowImpl,
|
|
232
|
+
sleepImpl
|
|
233
|
+
});
|
|
234
|
+
if (response === null) {
|
|
235
|
+
return { decision: 'timeout', requestId: remote.requestId, responseTopic: remote.responseTopic };
|
|
236
|
+
}
|
|
237
|
+
await sendResponseConfirmation({ config, response, title, fetchImpl });
|
|
238
|
+
return { ...response, requestId: remote.requestId, responseTopic: remote.responseTopic };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function requestApproval(options) {
|
|
242
|
+
return requestDecision({
|
|
243
|
+
...options,
|
|
244
|
+
options: [
|
|
245
|
+
{ value: 'allow', label: 'Allow' },
|
|
246
|
+
{ value: 'deny', label: 'Deny' }
|
|
247
|
+
]
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function requestRefinement(options) {
|
|
252
|
+
return requestDecision({ ...options, options: [], includeRefine: true });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function requestChoice({ options, ...rest }) {
|
|
256
|
+
const normalized = normalizeOptions(options);
|
|
257
|
+
if (normalized.length < 1 || normalized.length > 3) throw new Error('NOFAX_CHOICE_LIMIT');
|
|
258
|
+
return requestDecision({ ...rest, options: normalized });
|
|
259
|
+
}
|