@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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +706 -0
  3. package/dist/cjs/buffer.js +352 -0
  4. package/dist/cjs/client.js +396 -0
  5. package/dist/cjs/config.js +241 -0
  6. package/dist/cjs/crypto.js +288 -0
  7. package/dist/cjs/errors.js +96 -0
  8. package/dist/cjs/http.js +272 -0
  9. package/dist/cjs/index.js +74 -0
  10. package/dist/cjs/models.js +300 -0
  11. package/dist/cjs/package.json +1 -0
  12. package/dist/cjs/pump.js +279 -0
  13. package/dist/cjs/webhooks.js +335 -0
  14. package/dist/cjs/xml.js +257 -0
  15. package/dist/esm/buffer.js +348 -0
  16. package/dist/esm/client.js +392 -0
  17. package/dist/esm/config.js +237 -0
  18. package/dist/esm/crypto.js +281 -0
  19. package/dist/esm/errors.js +86 -0
  20. package/dist/esm/http.js +267 -0
  21. package/dist/esm/index.js +37 -0
  22. package/dist/esm/models.js +292 -0
  23. package/dist/esm/package.json +1 -0
  24. package/dist/esm/pump.js +275 -0
  25. package/dist/esm/webhooks.js +329 -0
  26. package/dist/esm/xml.js +252 -0
  27. package/dist/types/buffer.d.ts +109 -0
  28. package/dist/types/client.d.ts +150 -0
  29. package/dist/types/config.d.ts +86 -0
  30. package/dist/types/crypto.d.ts +125 -0
  31. package/dist/types/errors.d.ts +73 -0
  32. package/dist/types/http.d.ts +80 -0
  33. package/dist/types/index.d.ts +36 -0
  34. package/dist/types/models.d.ts +154 -0
  35. package/dist/types/pump.d.ts +118 -0
  36. package/dist/types/webhooks.d.ts +99 -0
  37. package/dist/types/xml.d.ts +42 -0
  38. package/docs/config.md +93 -0
  39. package/docs/errors.md +87 -0
  40. package/docs/model.md +141 -0
  41. package/docs/pump.md +130 -0
  42. package/docs/webhooks.md +140 -0
  43. 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;