@allus-fyi/company-data 0.0.3
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 -0
- package/README.md +706 -0
- package/dist/cjs/buffer.js +352 -0
- package/dist/cjs/client.js +396 -0
- package/dist/cjs/config.js +241 -0
- package/dist/cjs/crypto.js +288 -0
- package/dist/cjs/errors.js +96 -0
- package/dist/cjs/http.js +272 -0
- package/dist/cjs/index.js +74 -0
- package/dist/cjs/models.js +300 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/pump.js +279 -0
- package/dist/cjs/webhooks.js +335 -0
- package/dist/cjs/xml.js +257 -0
- package/dist/esm/buffer.js +348 -0
- package/dist/esm/client.js +392 -0
- package/dist/esm/config.js +237 -0
- package/dist/esm/crypto.js +281 -0
- package/dist/esm/errors.js +86 -0
- package/dist/esm/http.js +267 -0
- package/dist/esm/index.js +37 -0
- package/dist/esm/models.js +292 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/pump.js +275 -0
- package/dist/esm/webhooks.js +329 -0
- package/dist/esm/xml.js +252 -0
- package/dist/types/buffer.d.ts +109 -0
- package/dist/types/client.d.ts +150 -0
- package/dist/types/config.d.ts +86 -0
- package/dist/types/crypto.d.ts +125 -0
- package/dist/types/errors.d.ts +73 -0
- package/dist/types/http.d.ts +80 -0
- package/dist/types/index.d.ts +36 -0
- package/dist/types/models.d.ts +154 -0
- package/dist/types/pump.d.ts +118 -0
- package/dist/types/webhooks.d.ts +99 -0
- package/dist/types/xml.d.ts +42 -0
- package/docs/config.md +93 -0
- package/docs/errors.md +87 -0
- package/docs/model.md +141 -0
- package/docs/pump.md +130 -0
- package/docs/webhooks.md +140 -0
- package/package.json +54 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Durable plain-file buffer for the crash-safe changes pump.
|
|
4
|
+
*
|
|
5
|
+
* The changes feed is a server-side **drain-on-fetch queue**: a fetch returns up to
|
|
6
|
+
* N events and deletes those rows in the same transaction — the API keeps no copy.
|
|
7
|
+
* So a drained batch MUST be persisted locally BEFORE any delivery, or a consumer
|
|
8
|
+
* crash mid-batch loses events the API already deleted. This module is that
|
|
9
|
+
* persistence: a zero-dependency, plain-file buffer under `cacheDir`.
|
|
10
|
+
*
|
|
11
|
+
* Layout:
|
|
12
|
+
*
|
|
13
|
+
* <cacheDir>/pending/<seq>_<change_id>.json # one un-acked event, oldest-first
|
|
14
|
+
* <cacheDir>/deadletter/<seq>_<change_id>.json # events that exhausted retries
|
|
15
|
+
*
|
|
16
|
+
* - The stored event is the **raw hardened API event object** — its `value` /
|
|
17
|
+
* `value_url` is **CIPHERTEXT**, never the decrypted plaintext. No PII is ever
|
|
18
|
+
* written to disk ("ciphertext at rest").
|
|
19
|
+
* - `<seq>` is a zero-padded, monotonically increasing sequence number persisted
|
|
20
|
+
* in `<cacheDir>/.seq`. Because {@link FileBuffer.append} is called in drain
|
|
21
|
+
* order (oldest-first), sorting filenames lexicographically yields oldest-first
|
|
22
|
+
* — a stable order even if event `at` timestamps are missing or equal.
|
|
23
|
+
* - Writes are **crash-safe**: each file is written to a temp name, fsync'd,
|
|
24
|
+
* atomically renamed into place, and the containing directory is fsync'd — so a
|
|
25
|
+
* crash never leaves a half-written pending file.
|
|
26
|
+
* - `ack(id)` deletes the pending file; `deadLetter(id, error, attempts)` moves it
|
|
27
|
+
* to `deadletter/` with the error + attempt count appended. Neither re-fetches
|
|
28
|
+
* from the API (it already deleted the row) — the buffer is the only home.
|
|
29
|
+
*
|
|
30
|
+
* All operations are SYNCHRONOUS (Node `fs` + `fsyncSync`) so the fsync discipline
|
|
31
|
+
* is exact. The pump calls them between awaits.
|
|
32
|
+
*/
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.FileBuffer = void 0;
|
|
35
|
+
const node_fs_1 = require("node:fs");
|
|
36
|
+
const node_path_1 = require("node:path");
|
|
37
|
+
const PENDING_DIR = 'pending';
|
|
38
|
+
const DEADLETTER_DIR = 'deadletter';
|
|
39
|
+
const SEQ_FILE = '.seq';
|
|
40
|
+
// Width of the zero-padded sequence prefix. 16 digits keeps filenames sorting
|
|
41
|
+
// lexicographically up to ~10^16 appends — vastly beyond any real backlog.
|
|
42
|
+
const SEQ_WIDTH = 16;
|
|
43
|
+
function sanitizeId(changeId) {
|
|
44
|
+
const s = changeId != null ? String(changeId) : 'noid';
|
|
45
|
+
const cleaned = Array.from(s)
|
|
46
|
+
.map((c) => (/[A-Za-z0-9_-]/.test(c) ? c : '_'))
|
|
47
|
+
.join('');
|
|
48
|
+
return cleaned.length > 0 ? cleaned : 'noid';
|
|
49
|
+
}
|
|
50
|
+
function fsyncDir(path) {
|
|
51
|
+
let fd;
|
|
52
|
+
try {
|
|
53
|
+
fd = (0, node_fs_1.openSync)(path, 'r');
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Platform without dir fds — ignore.
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
(0, node_fs_1.fsyncSync)(fd);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// fs without dir fsync — ignore.
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
(0, node_fs_1.closeSync)(fd);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function uniqueTempName(prefix) {
|
|
70
|
+
return `${prefix}${process.pid}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
71
|
+
}
|
|
72
|
+
function atomicWriteJson(path, obj) {
|
|
73
|
+
const directory = (0, node_path_1.join)(path, '..');
|
|
74
|
+
const tmp = (0, node_path_1.join)(directory, uniqueTempName('.tmp_') + '.json');
|
|
75
|
+
try {
|
|
76
|
+
(0, node_fs_1.writeFileSync)(tmp, JSON.stringify(obj));
|
|
77
|
+
const fd = (0, node_fs_1.openSync)(tmp, 'r');
|
|
78
|
+
try {
|
|
79
|
+
(0, node_fs_1.fsyncSync)(fd);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
(0, node_fs_1.closeSync)(fd);
|
|
83
|
+
}
|
|
84
|
+
(0, node_fs_1.renameSync)(tmp, path); // atomic rename over any existing file
|
|
85
|
+
}
|
|
86
|
+
catch (exc) {
|
|
87
|
+
try {
|
|
88
|
+
(0, node_fs_1.unlinkSync)(tmp);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// ignore — never leak partials
|
|
92
|
+
}
|
|
93
|
+
throw exc;
|
|
94
|
+
}
|
|
95
|
+
// Durably record the rename in the directory entry.
|
|
96
|
+
fsyncDir(directory);
|
|
97
|
+
}
|
|
98
|
+
function atomicWriteInt(path, value) {
|
|
99
|
+
const directory = (0, node_path_1.join)(path, '..');
|
|
100
|
+
const tmp = (0, node_path_1.join)(directory, uniqueTempName('.tmp_seq_'));
|
|
101
|
+
try {
|
|
102
|
+
(0, node_fs_1.writeFileSync)(tmp, String(value));
|
|
103
|
+
const fd = (0, node_fs_1.openSync)(tmp, 'r');
|
|
104
|
+
try {
|
|
105
|
+
(0, node_fs_1.fsyncSync)(fd);
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
(0, node_fs_1.closeSync)(fd);
|
|
109
|
+
}
|
|
110
|
+
(0, node_fs_1.renameSync)(tmp, path);
|
|
111
|
+
}
|
|
112
|
+
catch (exc) {
|
|
113
|
+
try {
|
|
114
|
+
(0, node_fs_1.unlinkSync)(tmp);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// ignore
|
|
118
|
+
}
|
|
119
|
+
throw exc;
|
|
120
|
+
}
|
|
121
|
+
fsyncDir(directory);
|
|
122
|
+
}
|
|
123
|
+
function seqOf(name) {
|
|
124
|
+
const head = name.split('_', 1)[0];
|
|
125
|
+
const n = Number(head);
|
|
126
|
+
return Number.isInteger(n) ? n : null;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* A durable, ordered, ciphertext-at-rest event buffer under `cacheDir`.
|
|
130
|
+
*
|
|
131
|
+
* Re-instantiating a `FileBuffer` on the same `cacheDir` recovers whatever is on
|
|
132
|
+
* disk — that recovery is exactly the pump's replay-on-restart.
|
|
133
|
+
*/
|
|
134
|
+
class FileBuffer {
|
|
135
|
+
constructor(cacheDir) {
|
|
136
|
+
this.pendingDir = (0, node_path_1.join)(cacheDir, PENDING_DIR);
|
|
137
|
+
this.deadletterDir = (0, node_path_1.join)(cacheDir, DEADLETTER_DIR);
|
|
138
|
+
this.seqPath = (0, node_path_1.join)(cacheDir, SEQ_FILE);
|
|
139
|
+
(0, node_fs_1.mkdirSync)(this.pendingDir, { recursive: true });
|
|
140
|
+
(0, node_fs_1.mkdirSync)(this.deadletterDir, { recursive: true });
|
|
141
|
+
}
|
|
142
|
+
// ── sequence ─────────────────────────────────────────────────────────────
|
|
143
|
+
// Single-threaded JS — no lock needed around the seq counter.
|
|
144
|
+
nextSeq() {
|
|
145
|
+
let current = this.readSeq();
|
|
146
|
+
if (current === null) {
|
|
147
|
+
current = this.maxOnDiskSeq();
|
|
148
|
+
}
|
|
149
|
+
const next = current + 1;
|
|
150
|
+
this.writeSeq(next);
|
|
151
|
+
return next;
|
|
152
|
+
}
|
|
153
|
+
readSeq() {
|
|
154
|
+
try {
|
|
155
|
+
const raw = (0, node_fs_1.readFileSync)(this.seqPath, 'utf8').trim();
|
|
156
|
+
const n = Number(raw);
|
|
157
|
+
return Number.isInteger(n) ? n : null;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
writeSeq(value) {
|
|
164
|
+
atomicWriteInt(this.seqPath, value);
|
|
165
|
+
}
|
|
166
|
+
maxOnDiskSeq() {
|
|
167
|
+
let best = 0;
|
|
168
|
+
for (const d of [this.pendingDir, this.deadletterDir]) {
|
|
169
|
+
for (const name of (0, node_fs_1.readdirSync)(d)) {
|
|
170
|
+
const seq = seqOf(name);
|
|
171
|
+
if (seq !== null && seq > best)
|
|
172
|
+
best = seq;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return best;
|
|
176
|
+
}
|
|
177
|
+
// ── append / list / ack ──────────────────────────────────────────────────
|
|
178
|
+
/**
|
|
179
|
+
* Persist a drained batch (oldest-first), each in its own fsync'd file.
|
|
180
|
+
*
|
|
181
|
+
* Each event is stored verbatim (ciphertext value intact). Returns the list of
|
|
182
|
+
* pending filenames written. This is the backup the API no longer holds — it
|
|
183
|
+
* MUST complete before the pump delivers anything.
|
|
184
|
+
*/
|
|
185
|
+
append(events) {
|
|
186
|
+
const written = [];
|
|
187
|
+
for (const event of events) {
|
|
188
|
+
const seq = this.nextSeq();
|
|
189
|
+
const changeId = event && typeof event === 'object' ? event['id'] : undefined;
|
|
190
|
+
const name = `${String(seq).padStart(SEQ_WIDTH, '0')}_${sanitizeId(changeId)}.json`;
|
|
191
|
+
atomicWriteJson((0, node_path_1.join)(this.pendingDir, name), event);
|
|
192
|
+
written.push(name);
|
|
193
|
+
}
|
|
194
|
+
return written;
|
|
195
|
+
}
|
|
196
|
+
/** All un-acked events, oldest-first (by the sortable filename). */
|
|
197
|
+
pending() {
|
|
198
|
+
return this.pendingFiles().map((n) => this.readEvent(this.pendingDir, n));
|
|
199
|
+
}
|
|
200
|
+
pendingFiles() {
|
|
201
|
+
const names = (0, node_fs_1.readdirSync)(this.pendingDir).filter((n) => n.endsWith('.json') && !n.startsWith('.tmp_'));
|
|
202
|
+
names.sort(); // zero-padded seq prefix → lexicographic == oldest-first
|
|
203
|
+
return names;
|
|
204
|
+
}
|
|
205
|
+
readEvent(directory, name) {
|
|
206
|
+
return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(directory, name), 'utf8'));
|
|
207
|
+
}
|
|
208
|
+
findPendingFile(changeId) {
|
|
209
|
+
const target = sanitizeId(changeId);
|
|
210
|
+
for (const name of this.pendingFiles()) {
|
|
211
|
+
const rest = name.slice(name.indexOf('_') + 1);
|
|
212
|
+
if (rest === `${target}.json`)
|
|
213
|
+
return name;
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
/** Delete the pending file for `changeId` (the per-item ack). Idempotent. */
|
|
218
|
+
ack(changeId) {
|
|
219
|
+
const name = this.findPendingFile(changeId);
|
|
220
|
+
if (name === null)
|
|
221
|
+
return false;
|
|
222
|
+
try {
|
|
223
|
+
(0, node_fs_1.unlinkSync)((0, node_path_1.join)(this.pendingDir, name));
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return false; // already gone (idempotent)
|
|
227
|
+
}
|
|
228
|
+
fsyncDir(this.pendingDir);
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
// ── dead-letter ────────────────────────────────────────────────────────────
|
|
232
|
+
/**
|
|
233
|
+
* Move a poison event from pending → deadletter with error + attempts.
|
|
234
|
+
*
|
|
235
|
+
* Crash safety: the new dead-letter copy is written BEFORE the pending copy is
|
|
236
|
+
* unlinked — never lose. A crash between the two leaves the event in BOTH dirs,
|
|
237
|
+
* which is harmless: replay re-delivers it (the id-dedup handler absorbs the
|
|
238
|
+
* duplicate). Do NOT "fix" this by deleting-first.
|
|
239
|
+
*
|
|
240
|
+
* The event keeps its ciphertext value; the failure context is appended under a
|
|
241
|
+
* reserved key so it is never silently dropped.
|
|
242
|
+
*/
|
|
243
|
+
deadLetter(changeId, error, attempts) {
|
|
244
|
+
const name = this.findPendingFile(changeId);
|
|
245
|
+
if (name === null)
|
|
246
|
+
return false;
|
|
247
|
+
const event = this.readEvent(this.pendingDir, name);
|
|
248
|
+
const record = { ...event, _deadletter: { error: String(error), attempts: Math.trunc(attempts) } };
|
|
249
|
+
// Write the dead-letter copy FIRST (at-least-once safe).
|
|
250
|
+
atomicWriteJson((0, node_path_1.join)(this.deadletterDir, name), record);
|
|
251
|
+
try {
|
|
252
|
+
(0, node_fs_1.unlinkSync)((0, node_path_1.join)(this.pendingDir, name));
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// already gone — harmless
|
|
256
|
+
}
|
|
257
|
+
fsyncDir(this.pendingDir);
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
deadletterFiles() {
|
|
261
|
+
const names = (0, node_fs_1.readdirSync)(this.deadletterDir).filter((n) => n.endsWith('.json') && !n.startsWith('.tmp_'));
|
|
262
|
+
names.sort();
|
|
263
|
+
return names;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* All dead-lettered events, oldest-first.
|
|
267
|
+
*
|
|
268
|
+
* Each item is the stored (ciphertext) event with a flattened `error` and
|
|
269
|
+
* `attempts` lifted out of the reserved `_deadletter` block, plus the event's own
|
|
270
|
+
* `id` for convenience.
|
|
271
|
+
*/
|
|
272
|
+
deadLetters() {
|
|
273
|
+
const out = [];
|
|
274
|
+
for (const name of this.deadletterFiles()) {
|
|
275
|
+
const event = this.readEvent(this.deadletterDir, name);
|
|
276
|
+
const meta = event['_deadletter'] ?? {};
|
|
277
|
+
out.push({
|
|
278
|
+
...event,
|
|
279
|
+
error: meta.error != null ? String(meta.error) : null,
|
|
280
|
+
attempts: meta.attempts != null ? Number(meta.attempts) : null,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
findDeadletterFile(changeId) {
|
|
286
|
+
const target = sanitizeId(changeId);
|
|
287
|
+
for (const name of this.deadletterFiles()) {
|
|
288
|
+
const rest = name.slice(name.indexOf('_') + 1);
|
|
289
|
+
if (rest === `${target}.json`)
|
|
290
|
+
return name;
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Rewrite a dead-letter record IN PLACE with a refreshed error + attempts.
|
|
296
|
+
*
|
|
297
|
+
* Used by a still-failing re-drive (`retryDeadLetters`): the record stays in
|
|
298
|
+
* `deadletter/` and its failure context is updated atomically (temp file inside
|
|
299
|
+
* `deadletter/` → fsync → rename over the same path). It is NEVER routed back
|
|
300
|
+
* through `pending/`, so a crash anywhere in this method leaves the record either
|
|
301
|
+
* as the old dead-letter or the new one — it can never resurrect as a live
|
|
302
|
+
* pending event. Idempotent (returns false if the record is gone).
|
|
303
|
+
* Preserves the file's seq prefix so its oldest-first ordering is unchanged.
|
|
304
|
+
*
|
|
305
|
+
* The stored attempt count is monotonic across separate re-drive runs — a later
|
|
306
|
+
* run with a smaller `maxRetries` must never lower the recorded total — so we
|
|
307
|
+
* clamp to `max(existing, new)`.
|
|
308
|
+
*/
|
|
309
|
+
updateDeadLetter(changeId, error, attempts) {
|
|
310
|
+
const name = this.findDeadletterFile(changeId);
|
|
311
|
+
if (name === null)
|
|
312
|
+
return false;
|
|
313
|
+
const path = (0, node_path_1.join)(this.deadletterDir, name);
|
|
314
|
+
let event;
|
|
315
|
+
try {
|
|
316
|
+
event = this.readEvent(this.deadletterDir, name);
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return false; // already gone (idempotent)
|
|
320
|
+
}
|
|
321
|
+
const priorMeta = event['_deadletter'];
|
|
322
|
+
let priorAttempts = 0;
|
|
323
|
+
if (priorMeta && priorMeta.attempts != null) {
|
|
324
|
+
const n = Number(priorMeta.attempts);
|
|
325
|
+
priorAttempts = Number.isFinite(n) ? n : 0;
|
|
326
|
+
}
|
|
327
|
+
const record = {};
|
|
328
|
+
for (const [k, v] of Object.entries(event)) {
|
|
329
|
+
if (k === '_deadletter' || k === 'error' || k === 'attempts')
|
|
330
|
+
continue;
|
|
331
|
+
record[k] = v;
|
|
332
|
+
}
|
|
333
|
+
record['_deadletter'] = { error: String(error), attempts: Math.max(priorAttempts, Math.trunc(attempts)) };
|
|
334
|
+
atomicWriteJson(path, record); // temp+fsync+rename, all within deadletter/
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
/** Delete a dead-letter record (after a successful re-drive). Idempotent. */
|
|
338
|
+
removeDeadLetter(changeId) {
|
|
339
|
+
const name = this.findDeadletterFile(changeId);
|
|
340
|
+
if (name === null)
|
|
341
|
+
return false;
|
|
342
|
+
try {
|
|
343
|
+
(0, node_fs_1.unlinkSync)((0, node_path_1.join)(this.deadletterDir, name));
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
fsyncDir(this.deadletterDir);
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
exports.FileBuffer = FileBuffer;
|