@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,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client facade.
|
|
3
|
+
*
|
|
4
|
+
* The one object an integrating company touches. Build it from config (the keys
|
|
5
|
+
* live there and nowhere else), then call:
|
|
6
|
+
*
|
|
7
|
+
* client.requestFields() -> Promise<RequestField[]> (slug -> meta, cached)
|
|
8
|
+
* client.connections(limit, offset) -> AsyncIterable<Connection> (auto-paged, lazy)
|
|
9
|
+
* client.connection(id) -> Promise<Connection>
|
|
10
|
+
* client.logs(limit, offset) -> Promise<LogEntry[]>
|
|
11
|
+
* client.processChanges(handler, opts) -> Promise<void> (the crash-safe pump)
|
|
12
|
+
* client.drainBatch(max) -> Promise<Change[]> (raw unbuffered drain — advanced)
|
|
13
|
+
* client.deadLetters() / client.retryDeadLetters(handler)
|
|
14
|
+
*
|
|
15
|
+
* Plus the webhook receiver helpers, exposed as methods that delegate to the
|
|
16
|
+
* `webhooks` module (all config-driven, no key/secret args):
|
|
17
|
+
*
|
|
18
|
+
* client.verifyWebhook(rawBody, headers) -> bool
|
|
19
|
+
* client.parseWebhook(rawBody, headers) -> Change
|
|
20
|
+
* client.handleWebhook(rawBody, headers) -> Change
|
|
21
|
+
*
|
|
22
|
+
* How it is wired (everything else the SDK hides):
|
|
23
|
+
* - **Auth + transport** — an {@link HttpClient} owns the `client_credentials`
|
|
24
|
+
* token, the JSON/XML accept+parse, and the error mapping (incl. 429 backoff).
|
|
25
|
+
* - **Decryption** — the service private key is loaded **once** at construction
|
|
26
|
+
* from the configured encrypted PEM + passphrase into an in-memory key; a
|
|
27
|
+
* `decryptValue` closure over it is handed to every model factory and the pump
|
|
28
|
+
* (config-only key handling — the key never appears in a method signature).
|
|
29
|
+
* - **Slug catalog** — `requestFields()` is fetched once and cached; its slug→type
|
|
30
|
+
* map types every value (so `address` parses to an object, `photo` becomes a
|
|
31
|
+
* lazy binary handle, etc.).
|
|
32
|
+
* - **Binary** — a value's `BinaryHandle.bytes()` GETs the slot file endpoint,
|
|
33
|
+
* unwraps the API's `{"encrypted":true,"value":<wrapper>}` envelope, and runs
|
|
34
|
+
* the same service-key decrypt → the file bytes.
|
|
35
|
+
* - **Changes feed** — `processChanges` delegates to the {@link Pump}, injecting a
|
|
36
|
+
* `fetchChanges` closure (`GET /changes?limit=`, returning the raw ciphertext
|
|
37
|
+
* events) and a `decrypt` closure that builds a typed {@link Change}.
|
|
38
|
+
*/
|
|
39
|
+
import { Config } from './config.js';
|
|
40
|
+
import { HttpClient, type HttpClientOptions } from './http.js';
|
|
41
|
+
import { Change, Connection, LogEntry, RequestField } from './models.js';
|
|
42
|
+
import { Pump, type Handler, type Logger, type ProcessOptions } from './pump.js';
|
|
43
|
+
import type { DeadLetterRecord } from './buffer.js';
|
|
44
|
+
import { type Headers } from './webhooks.js';
|
|
45
|
+
export interface ClientOptions {
|
|
46
|
+
/** An injected transport/auth layer (for tests). */
|
|
47
|
+
http?: HttpClient;
|
|
48
|
+
/** Options forwarded to the default {@link HttpClient} when `http` is not supplied. */
|
|
49
|
+
httpOptions?: HttpClientOptions;
|
|
50
|
+
/** A logger sink for the pump (every drain/deliver/ack/retry/dead-letter/replay). */
|
|
51
|
+
logger?: Logger;
|
|
52
|
+
/** A sleep callable (seconds → Promise) — injectable for tests. */
|
|
53
|
+
sleep?: (seconds: number) => Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
/** The company-data SDK client facade. */
|
|
56
|
+
export declare class Client {
|
|
57
|
+
private readonly config;
|
|
58
|
+
private readonly log;
|
|
59
|
+
private readonly sleep;
|
|
60
|
+
private readonly http;
|
|
61
|
+
private readonly privateKey;
|
|
62
|
+
private readonly accountKey;
|
|
63
|
+
private cachedRequestFields;
|
|
64
|
+
private typeBySlug;
|
|
65
|
+
private requestFieldsInFlight;
|
|
66
|
+
private _pump;
|
|
67
|
+
constructor(config: Config, opts?: ClientOptions);
|
|
68
|
+
/** Build from a JSON config file (env vars override secrets). */
|
|
69
|
+
static fromConfig(path: string, opts?: ClientOptions): Client;
|
|
70
|
+
/** Build entirely from `ALLUS_*` env vars. */
|
|
71
|
+
static fromEnv(opts?: ClientOptions): Client;
|
|
72
|
+
private decryptValue;
|
|
73
|
+
/**
|
|
74
|
+
* Fetch a slot file endpoint and unwrap its `{"encrypted":true,"value":...}` envelope.
|
|
75
|
+
*
|
|
76
|
+
* Returns the inner `{"_enc":1,...}` wrapper, which the {@link BinaryHandle} then
|
|
77
|
+
* decrypts with the same service key.
|
|
78
|
+
*/
|
|
79
|
+
private binaryFetch;
|
|
80
|
+
/** Resolve a request slug to its field type (loads the catalog once). */
|
|
81
|
+
private typeForSlug;
|
|
82
|
+
/**
|
|
83
|
+
* The cached request-field DEFINITIONS.
|
|
84
|
+
*
|
|
85
|
+
* Fetched once from `GET /api/company-data/request-fields` and cached for the life
|
|
86
|
+
* of the client (it's the company's static config, and it types every value).
|
|
87
|
+
* Returns YOUR request config — never the person's fields. Concurrent callers
|
|
88
|
+
* share a single in-flight fetch.
|
|
89
|
+
*/
|
|
90
|
+
requestFields(): Promise<RequestField[]>;
|
|
91
|
+
/**
|
|
92
|
+
* A lazy async generator paging the list endpoint, yielding one Connection at a time.
|
|
93
|
+
*
|
|
94
|
+
* `limit` is the page size; `offset` the starting offset. The generator auto-pages
|
|
95
|
+
* `GET /api/company-data/connections?limit&offset` and yields typed
|
|
96
|
+
* {@link Connection} objects (each `values[slug]` already decrypted / a lazy binary
|
|
97
|
+
* handle) one at a time — bounded memory for a large book. It honors the response's
|
|
98
|
+
* `total` (when present) so it never over-fetches a page past the end, and also
|
|
99
|
+
* stops on a short page as a fallback.
|
|
100
|
+
*
|
|
101
|
+
* The connections endpoints are **heavily rate-limited**: use this
|
|
102
|
+
* for the initial full sync + occasional reconciliation, never as a poll substitute
|
|
103
|
+
* for the changes feed. On a surfaced {@link RateLimitError} the generator backs off
|
|
104
|
+
* per `Retry-After` and retries the page a bounded number of times before
|
|
105
|
+
* re-throwing — so it paces itself within the limit rather than hammering.
|
|
106
|
+
*/
|
|
107
|
+
connections(limit?: number, offset?: number): AsyncGenerator<Connection>;
|
|
108
|
+
private getConnectionsPage;
|
|
109
|
+
/**
|
|
110
|
+
* Fetch a single connection by id → one {@link Connection}.
|
|
111
|
+
*
|
|
112
|
+
* `GET /api/company-data/connections/{id}` returns `{connection_id, user_id,
|
|
113
|
+
* values}` and no displayName/connectedAt; those identity fields simply stay
|
|
114
|
+
* `null` (the list endpoint carries them).
|
|
115
|
+
*/
|
|
116
|
+
connection(id: string): Promise<Connection>;
|
|
117
|
+
/**
|
|
118
|
+
* The service's activity log → `LogEntry[]`.
|
|
119
|
+
*
|
|
120
|
+
* `GET /api/company-data/logs?limit&offset`. Ops events only (email / purge /
|
|
121
|
+
* webhook) — never person field data.
|
|
122
|
+
*/
|
|
123
|
+
logs(limit?: number, offset?: number): Promise<LogEntry[]>;
|
|
124
|
+
/** The crash-safe changes {@link Pump} (built lazily). */
|
|
125
|
+
get pump(): Pump;
|
|
126
|
+
private fetchChanges;
|
|
127
|
+
private decryptChange;
|
|
128
|
+
/**
|
|
129
|
+
* Drain the changes feed through `handler` one at a time, crash-safely.
|
|
130
|
+
*
|
|
131
|
+
* Delegates to the {@link Pump}: replay the durable buffer, drain ≤500 at a time,
|
|
132
|
+
* persist-before-deliver, per-item ack, retry→dead-letter→continue, until the feed
|
|
133
|
+
* is empty then return (no daemon mode — schedule re-runs yourself). `handler` must
|
|
134
|
+
* be idempotent (at-least-once; dedup on `Change.id`). Options:
|
|
135
|
+
* `batchSize` (≤500), `maxRetries`, `onError` (`deadletter`|`halt`), `backoff`.
|
|
136
|
+
*/
|
|
137
|
+
processChanges(handler: Handler, options?: ProcessOptions): Promise<void>;
|
|
138
|
+
/** Raw, UNBUFFERED drain → `Change[]` (advanced — you own durability). */
|
|
139
|
+
drainBatch(max?: number): Promise<Change[]>;
|
|
140
|
+
/** The local dead-letter store. */
|
|
141
|
+
deadLetters(): DeadLetterRecord[];
|
|
142
|
+
/** Re-drive dead-lettered events through `handler`. */
|
|
143
|
+
retryDeadLetters(handler: Handler, options?: ProcessOptions): Promise<number>;
|
|
144
|
+
/** Verify a webhook's `X-Allus-Signature` HMAC. */
|
|
145
|
+
verifyWebhook(rawBody: Buffer | Uint8Array | string, headers: Headers): boolean;
|
|
146
|
+
/** Parse a webhook body → a typed {@link Change}. */
|
|
147
|
+
parseWebhook(rawBody: Buffer | Uint8Array | string, headers: Headers): Change;
|
|
148
|
+
/** Verify + parse a webhook in one call → {@link Change}. */
|
|
149
|
+
handleWebhook(rawBody: Buffer | Uint8Array | string, headers: Headers): Change;
|
|
150
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration loading.
|
|
3
|
+
*
|
|
4
|
+
* Config-only key handling is a hard rule: **no SDK method ever takes a key,
|
|
5
|
+
* passphrase, or secret as an argument.** Everything cryptographic — decrypting
|
|
6
|
+
* the service PEM, decrypting field values, verifying the webhook HMAC, unwrapping
|
|
7
|
+
* the account-key envelope — is driven entirely by this config. The developer's
|
|
8
|
+
* only key responsibility is putting the right values here.
|
|
9
|
+
*
|
|
10
|
+
* A single JSON file holds everything; any field may be overridden by an `ALLUS_*`
|
|
11
|
+
* env var, so secrets needn't live in the file.
|
|
12
|
+
*/
|
|
13
|
+
export type WireFormat = 'json' | 'xml';
|
|
14
|
+
/** Reserved webhook-map key under which a flat `webhook_secret` is stored. */
|
|
15
|
+
export declare const SINGLE_WEBHOOK_KEY = "__single__";
|
|
16
|
+
/** HTTP Basic webhook-auth credentials ({@link Config.webhookBasic}). */
|
|
17
|
+
export interface WebhookBasic {
|
|
18
|
+
username: string;
|
|
19
|
+
password: string;
|
|
20
|
+
}
|
|
21
|
+
/** Custom-header webhook auth ({@link Config.webhookHeader}). */
|
|
22
|
+
export interface WebhookHeader {
|
|
23
|
+
name: string;
|
|
24
|
+
value: string;
|
|
25
|
+
}
|
|
26
|
+
/** The single configured webhook auth method, if any. */
|
|
27
|
+
export type WebhookAuthMethod = 'hmac' | 'bearer' | 'basic' | 'header' | 'none';
|
|
28
|
+
interface ConfigInit {
|
|
29
|
+
apiUrl: string;
|
|
30
|
+
clientId: string;
|
|
31
|
+
clientSecret: string;
|
|
32
|
+
servicePrivateKey: string;
|
|
33
|
+
keyPassphrase: string;
|
|
34
|
+
accountPrivateKey?: string | null;
|
|
35
|
+
accountPassphrase?: string | null;
|
|
36
|
+
webhooks?: Record<string, string>;
|
|
37
|
+
webhookBearerToken?: string | null;
|
|
38
|
+
webhookBasic?: WebhookBasic | null;
|
|
39
|
+
webhookHeader?: WebhookHeader | null;
|
|
40
|
+
webhookAuthNone?: boolean;
|
|
41
|
+
cacheDir?: string;
|
|
42
|
+
format?: WireFormat;
|
|
43
|
+
}
|
|
44
|
+
/** The whole SDK configuration. Keys live here and nowhere else. */
|
|
45
|
+
export declare class Config {
|
|
46
|
+
readonly apiUrl: string;
|
|
47
|
+
readonly clientId: string;
|
|
48
|
+
readonly clientSecret: string;
|
|
49
|
+
readonly servicePrivateKey: string;
|
|
50
|
+
readonly keyPassphrase: string;
|
|
51
|
+
readonly accountPrivateKey: string | null;
|
|
52
|
+
readonly accountPassphrase: string | null;
|
|
53
|
+
readonly webhooks: Record<string, string>;
|
|
54
|
+
readonly webhookBearerToken: string | null;
|
|
55
|
+
readonly webhookBasic: WebhookBasic | null;
|
|
56
|
+
readonly webhookHeader: WebhookHeader | null;
|
|
57
|
+
readonly webhookAuthNone: boolean;
|
|
58
|
+
readonly cacheDir: string;
|
|
59
|
+
readonly format: WireFormat;
|
|
60
|
+
/** Reserved webhook-map key under which a flat `webhook_secret` is stored. */
|
|
61
|
+
static readonly SINGLE_WEBHOOK_KEY = "__single__";
|
|
62
|
+
constructor(init: ConfigInit);
|
|
63
|
+
/** Load from a JSON file; env vars override file values. */
|
|
64
|
+
static fromFile(path: string): Config;
|
|
65
|
+
/** Build entirely from `ALLUS_*` env vars. */
|
|
66
|
+
static fromEnv(): Config;
|
|
67
|
+
/** Merge file values with env overrides, validate, and construct. */
|
|
68
|
+
private static build;
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the HMAC secret for a webhook id.
|
|
71
|
+
*
|
|
72
|
+
* Falls back to the single-webhook shortcut secret when there is no id or no
|
|
73
|
+
* id-specific match. The webhook helpers read this — application code never
|
|
74
|
+
* passes a secret in. (This method takes a webhook *id*, never a secret.)
|
|
75
|
+
*/
|
|
76
|
+
webhookSecret(webhookId?: string | null): string | null;
|
|
77
|
+
/**
|
|
78
|
+
* The single configured webhook auth method, or `null` if none is set.
|
|
79
|
+
*
|
|
80
|
+
* Returns one of `"hmac" | "bearer" | "basic" | "header" | "none"`. Config
|
|
81
|
+
* loading guarantees at most one is configured, so the order here is only a
|
|
82
|
+
* tie-break that never triggers.
|
|
83
|
+
*/
|
|
84
|
+
webhookAuthMethod(): WebhookAuthMethod | null;
|
|
85
|
+
}
|
|
86
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decryption core — byte-identical across all six SDKs.
|
|
3
|
+
*
|
|
4
|
+
* Every person value arrives as a ciphertext wrapper, encrypted **for the service
|
|
5
|
+
* public key**; the SDK decrypts with the service private key. The algorithm MUST
|
|
6
|
+
* match the platform's Web Crypto encryption exactly:
|
|
7
|
+
*
|
|
8
|
+
* wrapper = {"_enc":1,
|
|
9
|
+
* "k": base64(rsa_oaep_sha256(aesKey, servicePublicKey)),
|
|
10
|
+
* "iv": base64(iv12),
|
|
11
|
+
* "d": base64(aes256gcm_ciphertext_with_tag)}
|
|
12
|
+
*
|
|
13
|
+
* decrypt(wrapper, servicePrivateKey):
|
|
14
|
+
* aesKey = RSA-OAEP(SHA-256, MGF1-SHA256) decrypt wrapper.k // 32 bytes
|
|
15
|
+
* plaintext = AES-256-GCM decrypt wrapper.d with aesKey, iv=wrapper.iv
|
|
16
|
+
* // the 16-byte GCM tag is the LAST 16 bytes of d
|
|
17
|
+
* return utf8(plaintext)
|
|
18
|
+
*
|
|
19
|
+
* The service private key is the OpenSSL-encrypted PKCS#8 PEM downloaded from the
|
|
20
|
+
* portal (PBES2 = PBKDF2-HMAC-SHA256 + AES-256-CBC, ~100k iters). Node's
|
|
21
|
+
* `crypto.createPrivateKey({ key, passphrase })` reads it directly (PBES2 is
|
|
22
|
+
* handled by OpenSSL under the hood).
|
|
23
|
+
*
|
|
24
|
+
* Node specifics (the cross-language gotchas to watch for):
|
|
25
|
+
* - `crypto.privateDecrypt({ key, padding: RSA_PKCS1_OAEP_PADDING,
|
|
26
|
+
* oaepHash: 'sha256' }, k)` — **`oaepHash: 'sha256'` MUST be set explicitly**;
|
|
27
|
+
* Node defaults `oaepHash` to SHA-1, which would mismatch the platform and
|
|
28
|
+
* fail to unwrap the AES key. Setting it to sha256 also pins MGF1 to SHA-256.
|
|
29
|
+
* - `crypto.createDecipheriv('aes-256-gcm', aesKey, iv)` + `setAuthTag(tag)` —
|
|
30
|
+
* the 16-byte tag is the LAST 16 bytes of `d`.
|
|
31
|
+
*/
|
|
32
|
+
import { type KeyObject } from 'node:crypto';
|
|
33
|
+
export declare const GCM_TAG_LEN = 16;
|
|
34
|
+
export declare const GCM_IV_LEN = 12;
|
|
35
|
+
export { DecryptError } from './errors.js';
|
|
36
|
+
/** The platform hybrid wrapper `{"_enc":1,k,iv,d}`. */
|
|
37
|
+
export interface EncWrapper {
|
|
38
|
+
_enc?: number;
|
|
39
|
+
k: string;
|
|
40
|
+
iv: string;
|
|
41
|
+
d: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Load an OpenSSL-encrypted PKCS#8 PEM into an in-memory private key handle.
|
|
45
|
+
*
|
|
46
|
+
* The PEM is PBES2 (PBKDF2-HMAC-SHA256 + AES-256-CBC, ~100k iters). Node's
|
|
47
|
+
* `createPrivateKey` decrypts it with the passphrase (OpenSSL handles the SHA-256
|
|
48
|
+
* PRF). The key is never written back to disk in plaintext.
|
|
49
|
+
*
|
|
50
|
+
* Config-only key handling: this is the single place a passphrase is used, driven
|
|
51
|
+
* by `Config.keyPassphrase` — never passed in by application code.
|
|
52
|
+
*/
|
|
53
|
+
export declare function loadPrivateKey(encryptedPem: Buffer | string, passphrase: string): KeyObject;
|
|
54
|
+
/**
|
|
55
|
+
* Decrypt a platform `{"_enc":1,k,iv,d}` wrapper → a utf-8 plaintext string.
|
|
56
|
+
*
|
|
57
|
+
* For a *text* value the plaintext is the value itself. For a *binary* value the
|
|
58
|
+
* plaintext is a JSON envelope STRING (photo: `{"full":"data:...","thumb":...}`;
|
|
59
|
+
* document: `{"file":"data:...","original_name":...}`) — NOT raw bytes. The full
|
|
60
|
+
* binary-handle parse (envelope -> data-URI -> bytes) lives on {@link BinaryHandle};
|
|
61
|
+
* here we only ever decrypt to that envelope string.
|
|
62
|
+
*
|
|
63
|
+
* Throws {@link DecryptError} on a malformed wrapper, the wrong key, or a GCM tag
|
|
64
|
+
* mismatch.
|
|
65
|
+
*/
|
|
66
|
+
export declare function decrypt(wrapper: EncWrapper | string, privateKey: KeyObject): string;
|
|
67
|
+
/** Fetch a slot file endpoint → the inner `{"_enc":1,...}` wrapper. */
|
|
68
|
+
export type BinaryFetch = (valueUrl: string) => Promise<EncWrapper | string> | EncWrapper | string;
|
|
69
|
+
/** Decrypt a ciphertext wrapper → the envelope string (closes over the service key). */
|
|
70
|
+
export type DecryptWrapper = (wrapper: EncWrapper | string) => string;
|
|
71
|
+
/**
|
|
72
|
+
* Lazy handle for a binary (photo/document) value.
|
|
73
|
+
*
|
|
74
|
+
* A binary answer is stored server-side as a file, exposed in the hardened API as
|
|
75
|
+
* a slot-keyed `value_url` (never the source field). On `.bytes()` / `.save()` the
|
|
76
|
+
* handle GETs that URL, receives the `{"_enc":1,...}` wrapper, runs the same
|
|
77
|
+
* decrypt as text → a JSON envelope STRING (photo: `{"full":"data:...","thumb":...}`;
|
|
78
|
+
* document: `{"file":"data:...",...}`) — NOT raw bytes — then parses the envelope
|
|
79
|
+
* and base64-decodes the primary data-URI payload (`full` for photos, `file` for
|
|
80
|
+
* documents) into the file bytes.
|
|
81
|
+
*
|
|
82
|
+
* The fetch + decrypt are supplied by the client as plain callables (config-only
|
|
83
|
+
* key handling — no key is ever passed to this handle):
|
|
84
|
+
* - `valueUrl` + `fetch` — `fetch(valueUrl)` returns the encrypted wrapper (the
|
|
85
|
+
* client passes a callback that GETs the slot file endpoint and unwraps the
|
|
86
|
+
* `{"encrypted": true, "value": <wrapper>}` envelope to the inner wrapper).
|
|
87
|
+
* - `decrypt` — `decrypt(wrapper)` returns the decrypted envelope string (a
|
|
88
|
+
* closure over the loaded service private key).
|
|
89
|
+
*
|
|
90
|
+
* For the shared crypto test vector the decrypted envelope is already in hand, so
|
|
91
|
+
* a handle can also be built directly from `envelopeJson` (no fetch).
|
|
92
|
+
*/
|
|
93
|
+
export declare class BinaryHandle {
|
|
94
|
+
private envelopeJson;
|
|
95
|
+
private readonly _valueUrl;
|
|
96
|
+
private readonly fetch;
|
|
97
|
+
private readonly decryptWrapper;
|
|
98
|
+
constructor(opts?: {
|
|
99
|
+
envelopeJson?: string | null;
|
|
100
|
+
valueUrl?: string | null;
|
|
101
|
+
fetch?: BinaryFetch | null;
|
|
102
|
+
decrypt?: DecryptWrapper | null;
|
|
103
|
+
});
|
|
104
|
+
/** The slot-keyed file URL this handle fetches from (opaque to callers). */
|
|
105
|
+
get valueUrl(): string | null;
|
|
106
|
+
private resolveEnvelope;
|
|
107
|
+
/**
|
|
108
|
+
* Turn a decrypted binary envelope STRING into the primary file bytes.
|
|
109
|
+
*
|
|
110
|
+
* Photo envelope -> the `full` data-URI payload; document envelope -> the `file`
|
|
111
|
+
* data-URI payload. Throws {@link DecryptError} on a malformed envelope.
|
|
112
|
+
*/
|
|
113
|
+
static parseEnvelopeBytes(envelopeJson: string): Buffer;
|
|
114
|
+
/** Fetch (if needed), decrypt, and return the decoded primary file bytes. */
|
|
115
|
+
bytes(): Promise<Buffer>;
|
|
116
|
+
/**
|
|
117
|
+
* Write the decoded file bytes to `path`; returns the number of bytes written.
|
|
118
|
+
*
|
|
119
|
+
* Crash-safe (matching the buffer's atomic-write discipline): the
|
|
120
|
+
* bytes are written to a temp file in the same directory, fsync'd, and atomically
|
|
121
|
+
* renamed into place — so a crash mid-write never leaves a truncated output file
|
|
122
|
+
* (the destination is either the old file or the complete new one).
|
|
123
|
+
*/
|
|
124
|
+
save(path: string): Promise<number>;
|
|
125
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error taxonomy — the same names across all six SDKs.
|
|
3
|
+
*
|
|
4
|
+
* | Error | When |
|
|
5
|
+
* |--------------------------------|---------------------------------------------------|
|
|
6
|
+
* | ConfigError | Missing/invalid config or key file at construction (fail fast). |
|
|
7
|
+
* | AuthError | Token fetch/refresh failed (bad client_id/secret, revoked client). |
|
|
8
|
+
* | ApiError(status, errorKey,…) | Any non-2xx from the API; carries the HTTP status + the platform error_key + message. |
|
|
9
|
+
* | DecryptError | Wrapper malformed, wrong key, or GCM tag mismatch. |
|
|
10
|
+
* | WebhookError | Signature verification failed or an envelope couldn't be unwrapped. |
|
|
11
|
+
* | RateLimitError(retryAfter) | A 429 from a rate-limited endpoint (subclass of ApiError); carries Retry-After. |
|
|
12
|
+
*
|
|
13
|
+
* All errors extend a common {@link AllusError} base so a single `catch (e) { if (e
|
|
14
|
+
* instanceof AllusError) … }` captures the whole taxonomy. `DecryptError` is raised
|
|
15
|
+
* by the decryption core and re-exported here so the full taxonomy lives in one
|
|
16
|
+
* place.
|
|
17
|
+
*/
|
|
18
|
+
/** Base class for every SDK error. */
|
|
19
|
+
export declare class AllusError extends Error {
|
|
20
|
+
constructor(message?: string);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Missing or invalid configuration (or key file) at construction (fail fast).
|
|
24
|
+
*
|
|
25
|
+
* Canonical home for the error; the config + client layers throw it for a bad
|
|
26
|
+
* config file, a missing required field, an unreadable PEM, or a wrong passphrase.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ConfigError extends AllusError {
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The `client_credentials` token fetch or refresh failed.
|
|
32
|
+
*
|
|
33
|
+
* Thrown when `/oauth2/token` rejects the credentials, or when a 401 mid-flight
|
|
34
|
+
* survives the one automatic refresh-and-retry.
|
|
35
|
+
*/
|
|
36
|
+
export declare class AuthError extends AllusError {
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Any non-2xx from the API.
|
|
40
|
+
*
|
|
41
|
+
* Carries the HTTP `status`, the platform `errorKey` (when the body provided one),
|
|
42
|
+
* and a human-readable `message`. A transport failure (no HTTP response — e.g. a
|
|
43
|
+
* connection error) surfaces as `new ApiError(0, null, …)`.
|
|
44
|
+
*/
|
|
45
|
+
export declare class ApiError extends AllusError {
|
|
46
|
+
readonly status: number;
|
|
47
|
+
readonly errorKey: string | null;
|
|
48
|
+
/** The human-readable message (distinct from the formatted `Error.message`). */
|
|
49
|
+
readonly apiMessage: string | null;
|
|
50
|
+
constructor(status: number, errorKey?: string | null, message?: string | null);
|
|
51
|
+
}
|
|
52
|
+
/** Signature verification failed, or a webhook envelope couldn't be unwrapped. */
|
|
53
|
+
export declare class WebhookError extends AllusError {
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A 429 from a rate-limited endpoint.
|
|
57
|
+
*
|
|
58
|
+
* Subclass of {@link ApiError} with a fixed status of 429; carries the
|
|
59
|
+
* `retryAfter` value parsed from the `Retry-After` response header (seconds, or
|
|
60
|
+
* `null` when absent).
|
|
61
|
+
*/
|
|
62
|
+
export declare class RateLimitError extends ApiError {
|
|
63
|
+
readonly retryAfter: number | null;
|
|
64
|
+
constructor(retryAfter?: number | null, errorKey?: string | null, message?: string | null);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Wrapper malformed, wrong key, or GCM tag mismatch.
|
|
68
|
+
*
|
|
69
|
+
* Defined here (rather than in `crypto.ts`) so the whole taxonomy is importable
|
|
70
|
+
* from one module; the decryption core imports + throws it.
|
|
71
|
+
*/
|
|
72
|
+
export declare class DecryptError extends AllusError {
|
|
73
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth token + HTTP layer.
|
|
3
|
+
*
|
|
4
|
+
* The {@link HttpClient} is the thin transport every higher layer goes through. It
|
|
5
|
+
* owns:
|
|
6
|
+
*
|
|
7
|
+
* - **Auth** — `client_credentials` only. On the first call (or when the cached
|
|
8
|
+
* token is near expiry) it POSTs `client_id`/`client_secret` to
|
|
9
|
+
* `{api_url}/oauth2/token` and caches the bearer token + its expiry. Refresh is
|
|
10
|
+
* automatic and transparent; a 401 mid-flight triggers exactly one
|
|
11
|
+
* refresh-and-retry, then surfaces as {@link AuthError}.
|
|
12
|
+
* - **Format** — sets `Accept` per `config.format` (`application/json` or
|
|
13
|
+
* `application/xml`) and parses the body accordingly. The XML parser is the
|
|
14
|
+
* XXE-safe `parseXml` (mirrors the platform serializer).
|
|
15
|
+
* - **Errors** — maps non-2xx to the error taxonomy: a 401 → refresh+retry then
|
|
16
|
+
* {@link AuthError}; a 429 → read `Retry-After` and back off + retry a bounded
|
|
17
|
+
* number of times, then {@link RateLimitError}; any other non-2xx →
|
|
18
|
+
* {@link ApiError} carrying the body's `error_key` when present.
|
|
19
|
+
*
|
|
20
|
+
* Config-only key handling: the client id/secret come from the {@link Config} —
|
|
21
|
+
* never a method argument.
|
|
22
|
+
*
|
|
23
|
+
* The transport is injectable (`HttpTransport`) so the whole client is testable
|
|
24
|
+
* without the network; the default uses Node's global `fetch`.
|
|
25
|
+
*/
|
|
26
|
+
import { Config } from './config.js';
|
|
27
|
+
/** A minimal HTTP response shape (a subset of the Fetch API `Response`). */
|
|
28
|
+
export interface HttpResponse {
|
|
29
|
+
status: number;
|
|
30
|
+
text(): Promise<string>;
|
|
31
|
+
/** Case-insensitive header lookup, returning `null` when absent (Fetch `Headers`). */
|
|
32
|
+
headers: {
|
|
33
|
+
get(name: string): string | null;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** A pluggable transport (the default wraps Node's global `fetch`). */
|
|
37
|
+
export interface HttpTransport {
|
|
38
|
+
post(url: string, form: Record<string, string>, headers: Record<string, string>): Promise<HttpResponse>;
|
|
39
|
+
get(url: string, params: Record<string, string | number> | undefined, headers: Record<string, string>): Promise<HttpResponse>;
|
|
40
|
+
}
|
|
41
|
+
export type Sleep = (seconds: number) => Promise<void>;
|
|
42
|
+
export type Clock = () => number;
|
|
43
|
+
/** Default transport over Node's global `fetch`. */
|
|
44
|
+
export declare class FetchTransport implements HttpTransport {
|
|
45
|
+
post(url: string, form: Record<string, string>, headers: Record<string, string>): Promise<HttpResponse>;
|
|
46
|
+
get(url: string, params: Record<string, string | number> | undefined, headers: Record<string, string>): Promise<HttpResponse>;
|
|
47
|
+
}
|
|
48
|
+
export interface HttpClientOptions {
|
|
49
|
+
transport?: HttpTransport;
|
|
50
|
+
sleep?: Sleep;
|
|
51
|
+
clock?: Clock;
|
|
52
|
+
maxRetries429?: number;
|
|
53
|
+
}
|
|
54
|
+
/** Authenticated JSON/XML transport for the company-data API. */
|
|
55
|
+
export declare class HttpClient {
|
|
56
|
+
private readonly config;
|
|
57
|
+
private readonly transport;
|
|
58
|
+
private readonly sleep;
|
|
59
|
+
private readonly clock;
|
|
60
|
+
private readonly maxRetries429;
|
|
61
|
+
private readonly apiUrl;
|
|
62
|
+
private token;
|
|
63
|
+
private tokenExpiry;
|
|
64
|
+
constructor(config: Config, opts?: HttpClientOptions);
|
|
65
|
+
private tokenValid;
|
|
66
|
+
private fetchToken;
|
|
67
|
+
private bearer;
|
|
68
|
+
/**
|
|
69
|
+
* GET `path` (e.g. `/api/company-data/connections`) → parsed body.
|
|
70
|
+
*
|
|
71
|
+
* Adds the bearer token + an `Accept` header matching `config.format`, parses
|
|
72
|
+
* JSON or XML, and maps non-2xx responses to the error taxonomy: 401 → one
|
|
73
|
+
* refresh-and-retry then {@link AuthError}; 429 → bounded Retry-After backoff
|
|
74
|
+
* then {@link RateLimitError}; other non-2xx → {@link ApiError} (carrying the
|
|
75
|
+
* body's `error_key` when present).
|
|
76
|
+
*/
|
|
77
|
+
get(path: string, params?: Record<string, string | number>): Promise<unknown>;
|
|
78
|
+
private url;
|
|
79
|
+
private parseBody;
|
|
80
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* allus company-data SDK for TypeScript / Node — one of six language ports that
|
|
3
|
+
* share an identical API surface.
|
|
4
|
+
*
|
|
5
|
+
* This package wraps the allus company-data API: point it at a JSON config file and
|
|
6
|
+
* it hands back typed, plaintext, your-slug-keyed conclusions with transparent
|
|
7
|
+
* hybrid decryption.
|
|
8
|
+
*
|
|
9
|
+
* Exported: config loading, the decryption core, the full error taxonomy, the
|
|
10
|
+
* HTTP/auth layer, the output model, the crash-safe changes pump (durable file
|
|
11
|
+
* buffer + pump), the `Client` facade, and the webhook receiver helpers. `Client`
|
|
12
|
+
* is the one object an integrating company touches.
|
|
13
|
+
*
|
|
14
|
+
* Both ESM (`import { Client } from '@allus/company-data'`) and CommonJS
|
|
15
|
+
* (`const { Client } = require('@allus/company-data')`) are supported via the
|
|
16
|
+
* package's `exports` map.
|
|
17
|
+
*/
|
|
18
|
+
export { Client } from './client.js';
|
|
19
|
+
export type { ClientOptions } from './client.js';
|
|
20
|
+
export { Config, SINGLE_WEBHOOK_KEY } from './config.js';
|
|
21
|
+
export type { WireFormat, WebhookBasic, WebhookHeader, WebhookAuthMethod } from './config.js';
|
|
22
|
+
export { loadPrivateKey, decrypt, BinaryHandle, GCM_IV_LEN, GCM_TAG_LEN } from './crypto.js';
|
|
23
|
+
export type { EncWrapper, BinaryFetch, DecryptWrapper } from './crypto.js';
|
|
24
|
+
export { AllusError, ConfigError, AuthError, ApiError, DecryptError, WebhookError, RateLimitError, } from './errors.js';
|
|
25
|
+
export { HttpClient, FetchTransport } from './http.js';
|
|
26
|
+
export type { HttpTransport, HttpResponse, HttpClientOptions, Sleep, Clock } from './http.js';
|
|
27
|
+
export { RequestField, Connection, Value, Change, LogEntry, STRUCTURED_TYPES, BINARY_TYPES, DATE_TYPES, } from './models.js';
|
|
28
|
+
export type { TypeForSlug } from './models.js';
|
|
29
|
+
export { FileBuffer } from './buffer.js';
|
|
30
|
+
export type { BufferedEvent, DeadLetterRecord } from './buffer.js';
|
|
31
|
+
export { Pump, MAX_BATCH } from './pump.js';
|
|
32
|
+
export type { FetchChanges, DecryptChange, Handler, Logger, OnError, ProcessOptions, PumpOptions, } from './pump.js';
|
|
33
|
+
export { verifyWebhook, parseWebhook, handleWebhook, loadAccountKey } from './webhooks.js';
|
|
34
|
+
export type { Headers } from './webhooks.js';
|
|
35
|
+
export { parseXml, XmlParseError } from './xml.js';
|
|
36
|
+
export declare const VERSION = "0.1.0";
|