@seamward/collector 0.1.0-alpha.2
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/README.md +307 -0
- package/dist/connect.d.ts +49 -0
- package/dist/connect.js +84 -0
- package/dist/connect.js.map +1 -0
- package/dist/deployment-context.d.ts +8 -0
- package/dist/deployment-context.js +43 -0
- package/dist/deployment-context.js.map +1 -0
- package/dist/envelope-builder.d.ts +51 -0
- package/dist/envelope-builder.js +72 -0
- package/dist/envelope-builder.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/observe.d.ts +110 -0
- package/dist/observe.js +294 -0
- package/dist/observe.js.map +1 -0
- package/dist/redact.d.ts +31 -0
- package/dist/redact.js +61 -0
- package/dist/redact.js.map +1 -0
- package/dist/shipper.d.ts +48 -0
- package/dist/shipper.js +79 -0
- package/dist/shipper.js.map +1 -0
- package/package.json +33 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { DeploymentContext, Json } from "@seamward/contracts";
|
|
2
|
+
import { type ObservationInput, type BuilderDeps } from "./envelope-builder.js";
|
|
3
|
+
import type { RedactionPolicy } from "./redact.js";
|
|
4
|
+
import { type ShipperConfig, type ShipperStats } from "./shipper.js";
|
|
5
|
+
/**
|
|
6
|
+
* The customer-facing surface. One collector per Integration boundary:
|
|
7
|
+
*
|
|
8
|
+
* const collector = createCollector({ ... });
|
|
9
|
+
* fastify.post("/webhooks/candidate", collector.observeWebhook(
|
|
10
|
+
* { integrationId: "int_ats", routeTemplate: "/webhooks/candidate" },
|
|
11
|
+
* async (payload) => { ...handle...; return { statusCode: 202 }; },
|
|
12
|
+
* ));
|
|
13
|
+
*
|
|
14
|
+
* Fail-open everywhere: record() and the wrapper never throw Seamward
|
|
15
|
+
* errors into the host. A handler's own exception is recorded (500, not
|
|
16
|
+
* accepted) and RETHROWN - the host's error semantics are untouched.
|
|
17
|
+
*/
|
|
18
|
+
export interface CollectorConfig extends Pick<ShipperConfig, "endpoint" | "credentialId" | "secret" | "sourceKey" | "ingestToken" | "maxBatchSize" | "flushIntervalMs" | "maxQueueSize" | "fetchFn" | "nowSec"> {
|
|
19
|
+
tenantId: string;
|
|
20
|
+
environmentId: string;
|
|
21
|
+
policy: RedactionPolicy;
|
|
22
|
+
/** Explicit values override supported deployment-platform variables. */
|
|
23
|
+
deployment?: Partial<DeploymentContext>;
|
|
24
|
+
/** Injected clock/id for tests. */
|
|
25
|
+
builderDeps?: BuilderDeps;
|
|
26
|
+
/** Monotonic elapsed-time clock. Defaults to performance.now(). */
|
|
27
|
+
monotonicNowMs?: () => number;
|
|
28
|
+
}
|
|
29
|
+
export type RecordInput = Omit<ObservationInput, "tenantId" | "environmentId" | "deployment">;
|
|
30
|
+
export interface WebhookMeta {
|
|
31
|
+
integrationId: string;
|
|
32
|
+
routeTemplate: string;
|
|
33
|
+
method?: ObservationInput["method"];
|
|
34
|
+
attempt?: number;
|
|
35
|
+
correlation?: ObservationInput["correlation"];
|
|
36
|
+
}
|
|
37
|
+
export interface WebhookResult {
|
|
38
|
+
statusCode?: number;
|
|
39
|
+
eventType?: ObservationInput["eventType"];
|
|
40
|
+
outcome?: ObservationInput["outcome"];
|
|
41
|
+
correlation?: ObservationInput["correlation"];
|
|
42
|
+
}
|
|
43
|
+
export interface FetchMeta {
|
|
44
|
+
integrationId: string;
|
|
45
|
+
/** Stable route template; never pass concrete IDs or query values. */
|
|
46
|
+
routeTemplate: string;
|
|
47
|
+
eventType?: ObservationInput["eventType"];
|
|
48
|
+
/** Overrides the request method when a Request-like input obscures it. */
|
|
49
|
+
method?: ObservationInput["method"];
|
|
50
|
+
attempt?: number;
|
|
51
|
+
correlation?: ObservationInput["correlation"];
|
|
52
|
+
/** Select request-side contract evidence instead of the response body. */
|
|
53
|
+
payloadLocation?: "request" | "response";
|
|
54
|
+
/**
|
|
55
|
+
* Optional local-only request value for body types that cannot be cloned or
|
|
56
|
+
* parsed safely. Values are never added to an envelope.
|
|
57
|
+
*/
|
|
58
|
+
requestPayload?: Json;
|
|
59
|
+
}
|
|
60
|
+
export interface QueueMeta {
|
|
61
|
+
integrationId: string;
|
|
62
|
+
/** Stable queue, topic, or subscription label. Never use a message ID. */
|
|
63
|
+
queueName: string;
|
|
64
|
+
eventType?: ObservationInput["eventType"];
|
|
65
|
+
attempt?: number;
|
|
66
|
+
correlation?: ObservationInput["correlation"];
|
|
67
|
+
}
|
|
68
|
+
export interface QueueResult {
|
|
69
|
+
disposition?: "acknowledged" | "rejected";
|
|
70
|
+
eventType?: ObservationInput["eventType"];
|
|
71
|
+
outcome?: ObservationInput["outcome"];
|
|
72
|
+
correlation?: ObservationInput["correlation"];
|
|
73
|
+
}
|
|
74
|
+
export interface ScheduledFeedMeta {
|
|
75
|
+
integrationId: string;
|
|
76
|
+
/** Stable feed or job label. Never use a run ID or timestamp. */
|
|
77
|
+
feedName: string;
|
|
78
|
+
direction: ObservationInput["direction"];
|
|
79
|
+
eventType?: ObservationInput["eventType"];
|
|
80
|
+
attempt?: number;
|
|
81
|
+
correlation?: ObservationInput["correlation"];
|
|
82
|
+
}
|
|
83
|
+
export interface ScheduledFeedResult {
|
|
84
|
+
disposition?: "completed" | "rejected";
|
|
85
|
+
eventType?: ObservationInput["eventType"];
|
|
86
|
+
outcome?: ObservationInput["outcome"];
|
|
87
|
+
correlation?: ObservationInput["correlation"];
|
|
88
|
+
}
|
|
89
|
+
export type ObservedFetch = (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => Promise<Response>;
|
|
90
|
+
export interface CollectorStats extends ShipperStats {
|
|
91
|
+
buildErrors: number;
|
|
92
|
+
}
|
|
93
|
+
export interface Collector {
|
|
94
|
+
/** Record one observation. Never throws. */
|
|
95
|
+
record(input: RecordInput): void;
|
|
96
|
+
/** Wrap an inbound webhook handler; observation is recorded around it. */
|
|
97
|
+
observeWebhook<R extends WebhookResult | void>(meta: WebhookMeta, handler: (payload: Json) => R | Promise<R>): (payload: Json) => Promise<R>;
|
|
98
|
+
/** Wrap outbound HTTP calls without delaying response-body consumption. */
|
|
99
|
+
observeFetch(meta: FetchMeta, fetchFn?: typeof fetch): ObservedFetch;
|
|
100
|
+
/** Wrap an outbound queue publish. Host errors are recorded and rethrown. */
|
|
101
|
+
observeQueuePublish<R extends QueueResult | void>(meta: QueueMeta, publisher: (message: Json) => R | Promise<R>): (message: Json) => Promise<R>;
|
|
102
|
+
/** Wrap an inbound queue consumer. Rejections may be returned explicitly. */
|
|
103
|
+
observeQueueConsumer<R extends QueueResult | void>(meta: QueueMeta, consumer: (message: Json) => R | Promise<R>): (message: Json) => Promise<R>;
|
|
104
|
+
/** Wrap one scheduled import or export run. */
|
|
105
|
+
observeScheduledFeed<R extends ScheduledFeedResult | void>(meta: ScheduledFeedMeta, handler: (payload: Json) => R | Promise<R>): (payload: Json) => Promise<R>;
|
|
106
|
+
flush(): Promise<void>;
|
|
107
|
+
stop(): Promise<void>;
|
|
108
|
+
stats(): CollectorStats;
|
|
109
|
+
}
|
|
110
|
+
export declare function createCollector(config: CollectorConfig): Collector;
|
package/dist/observe.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { resolveDeploymentContext } from "./deployment-context.js";
|
|
2
|
+
import { buildEnvelope, } from "./envelope-builder.js";
|
|
3
|
+
import { createShipper, } from "./shipper.js";
|
|
4
|
+
export function createCollector(config) {
|
|
5
|
+
const { tenantId, environmentId, policy, deployment: explicitDeployment, builderDeps, monotonicNowMs: configuredMonotonicNowMs, ...shipperConfig } = config;
|
|
6
|
+
const deployment = resolveDeploymentContext(explicitDeployment);
|
|
7
|
+
const shipper = createShipper(shipperConfig);
|
|
8
|
+
let buildErrors = 0;
|
|
9
|
+
const pendingInspections = new Set();
|
|
10
|
+
const monotonicNowMs = configuredMonotonicNowMs ?? (() => performance.now());
|
|
11
|
+
function buildAndEnqueue(input) {
|
|
12
|
+
try {
|
|
13
|
+
shipper.enqueue(buildEnvelope({
|
|
14
|
+
...input,
|
|
15
|
+
tenantId,
|
|
16
|
+
environmentId,
|
|
17
|
+
...(deployment ? { deployment } : {}),
|
|
18
|
+
}, policy, builderDeps));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Fail-open: a malformed observation must never break the host app.
|
|
22
|
+
buildErrors += 1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function record(input) {
|
|
26
|
+
trackInspection(new Promise((resolve) => {
|
|
27
|
+
setTimeout(() => {
|
|
28
|
+
buildAndEnqueue(input);
|
|
29
|
+
resolve();
|
|
30
|
+
}, 0);
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
function observeWebhook(meta, handler) {
|
|
34
|
+
return async (payload) => {
|
|
35
|
+
const startedAt = monotonicNowMs();
|
|
36
|
+
const base = {
|
|
37
|
+
integrationId: meta.integrationId,
|
|
38
|
+
direction: "inbound",
|
|
39
|
+
protocol: "http-webhook",
|
|
40
|
+
method: meta.method ?? "POST",
|
|
41
|
+
routeTemplate: meta.routeTemplate,
|
|
42
|
+
payload,
|
|
43
|
+
...(meta.attempt !== undefined ? { attempt: meta.attempt } : {}),
|
|
44
|
+
...(meta.correlation ? { correlation: meta.correlation } : {}),
|
|
45
|
+
};
|
|
46
|
+
try {
|
|
47
|
+
const result = await handler(payload);
|
|
48
|
+
record({
|
|
49
|
+
...base,
|
|
50
|
+
statusCode: result?.statusCode ?? 200,
|
|
51
|
+
durationMs: monotonicNowMs() - startedAt,
|
|
52
|
+
...(result?.eventType ? { eventType: result.eventType } : {}),
|
|
53
|
+
...(result?.outcome ? { outcome: result.outcome } : {}),
|
|
54
|
+
...(result?.correlation ? { correlation: result.correlation } : {}),
|
|
55
|
+
});
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
record({
|
|
60
|
+
...base,
|
|
61
|
+
statusCode: 500,
|
|
62
|
+
durationMs: monotonicNowMs() - startedAt,
|
|
63
|
+
outcome: { accepted: false },
|
|
64
|
+
});
|
|
65
|
+
throw error; // host error semantics untouched
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function trackInspection(inspection) {
|
|
70
|
+
let tracked;
|
|
71
|
+
tracked = inspection
|
|
72
|
+
.catch(() => { })
|
|
73
|
+
.finally(() => {
|
|
74
|
+
pendingInspections.delete(tracked);
|
|
75
|
+
});
|
|
76
|
+
pendingInspections.add(tracked);
|
|
77
|
+
}
|
|
78
|
+
async function waitForInspections() {
|
|
79
|
+
while (pendingInspections.size > 0) {
|
|
80
|
+
await Promise.all([...pendingInspections]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function observeFetch(meta, providerFetch = fetch) {
|
|
84
|
+
return async (input, init) => {
|
|
85
|
+
const startedAt = monotonicNowMs();
|
|
86
|
+
const payloadLocation = meta.payloadLocation ?? "response";
|
|
87
|
+
const requestInspection = payloadLocation === "request"
|
|
88
|
+
? prepareRequestPayloadInspection(input, init, meta.requestPayload)
|
|
89
|
+
: undefined;
|
|
90
|
+
const requestedMethod = meta.method ??
|
|
91
|
+
init?.method ??
|
|
92
|
+
(typeof Request !== "undefined" && input instanceof Request
|
|
93
|
+
? input.method
|
|
94
|
+
: "GET");
|
|
95
|
+
const method = requestedMethod.toUpperCase();
|
|
96
|
+
const base = {
|
|
97
|
+
integrationId: meta.integrationId,
|
|
98
|
+
direction: "outbound",
|
|
99
|
+
protocol: "http-api",
|
|
100
|
+
method,
|
|
101
|
+
routeTemplate: meta.routeTemplate,
|
|
102
|
+
...(meta.eventType ? { eventType: meta.eventType } : {}),
|
|
103
|
+
...(meta.attempt !== undefined ? { attempt: meta.attempt } : {}),
|
|
104
|
+
...(meta.correlation ? { correlation: meta.correlation } : {}),
|
|
105
|
+
};
|
|
106
|
+
let response;
|
|
107
|
+
try {
|
|
108
|
+
response = await providerFetch(input, init);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
const durationMs = monotonicNowMs() - startedAt;
|
|
112
|
+
if (requestInspection) {
|
|
113
|
+
trackInspection(requestInspection.then((payload) => {
|
|
114
|
+
record({
|
|
115
|
+
...base,
|
|
116
|
+
payloadLocation,
|
|
117
|
+
statusCode: 0,
|
|
118
|
+
durationMs,
|
|
119
|
+
...(payload !== undefined ? { payload } : {}),
|
|
120
|
+
outcome: { accepted: false },
|
|
121
|
+
});
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
record({
|
|
126
|
+
...base,
|
|
127
|
+
payloadLocation,
|
|
128
|
+
statusCode: 0,
|
|
129
|
+
durationMs,
|
|
130
|
+
outcome: { accepted: false },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
const durationMs = monotonicNowMs() - startedAt;
|
|
136
|
+
// Clone and inspect asynchronously. The original response is returned as
|
|
137
|
+
// soon as the provider fetch resolves, preserving host response timing.
|
|
138
|
+
trackInspection(Promise.resolve().then(async () => {
|
|
139
|
+
let payload;
|
|
140
|
+
try {
|
|
141
|
+
payload =
|
|
142
|
+
payloadLocation === "request"
|
|
143
|
+
? await requestInspection
|
|
144
|
+
: (await response.clone().json());
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Empty or non-JSON responses still produce transport evidence.
|
|
148
|
+
}
|
|
149
|
+
record({
|
|
150
|
+
...base,
|
|
151
|
+
payloadLocation,
|
|
152
|
+
statusCode: response.status,
|
|
153
|
+
durationMs,
|
|
154
|
+
...(payload !== undefined ? { payload } : {}),
|
|
155
|
+
outcome: { accepted: response.ok },
|
|
156
|
+
});
|
|
157
|
+
}));
|
|
158
|
+
return response;
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function observeQueue(direction, meta, handler) {
|
|
162
|
+
return async (message) => {
|
|
163
|
+
const startedAt = monotonicNowMs();
|
|
164
|
+
const base = {
|
|
165
|
+
integrationId: meta.integrationId,
|
|
166
|
+
direction,
|
|
167
|
+
protocol: "queue",
|
|
168
|
+
method: "POST",
|
|
169
|
+
routeTemplate: meta.queueName,
|
|
170
|
+
payloadLocation: "message",
|
|
171
|
+
payload: message,
|
|
172
|
+
...(meta.eventType ? { eventType: meta.eventType } : {}),
|
|
173
|
+
...(meta.attempt !== undefined ? { attempt: meta.attempt } : {}),
|
|
174
|
+
...(meta.correlation ? { correlation: meta.correlation } : {}),
|
|
175
|
+
};
|
|
176
|
+
try {
|
|
177
|
+
const result = await handler(message);
|
|
178
|
+
const rejected = result?.disposition === "rejected";
|
|
179
|
+
record({
|
|
180
|
+
...base,
|
|
181
|
+
statusCode: rejected ? 422 : 202,
|
|
182
|
+
durationMs: monotonicNowMs() - startedAt,
|
|
183
|
+
...(result?.eventType ? { eventType: result.eventType } : {}),
|
|
184
|
+
...(result?.correlation ? { correlation: result.correlation } : {}),
|
|
185
|
+
outcome: result?.outcome ?? { accepted: !rejected },
|
|
186
|
+
});
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
record({
|
|
191
|
+
...base,
|
|
192
|
+
statusCode: 500,
|
|
193
|
+
durationMs: monotonicNowMs() - startedAt,
|
|
194
|
+
outcome: { accepted: false },
|
|
195
|
+
});
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function observeQueuePublish(meta, publisher) {
|
|
201
|
+
return observeQueue("outbound", meta, publisher);
|
|
202
|
+
}
|
|
203
|
+
function observeQueueConsumer(meta, consumer) {
|
|
204
|
+
return observeQueue("inbound", meta, consumer);
|
|
205
|
+
}
|
|
206
|
+
function observeScheduledFeed(meta, handler) {
|
|
207
|
+
return async (payload) => {
|
|
208
|
+
const startedAt = monotonicNowMs();
|
|
209
|
+
const base = {
|
|
210
|
+
integrationId: meta.integrationId,
|
|
211
|
+
direction: meta.direction,
|
|
212
|
+
protocol: "scheduled-feed",
|
|
213
|
+
method: "POST",
|
|
214
|
+
routeTemplate: meta.feedName,
|
|
215
|
+
payloadLocation: "message",
|
|
216
|
+
payload,
|
|
217
|
+
...(meta.eventType ? { eventType: meta.eventType } : {}),
|
|
218
|
+
...(meta.attempt !== undefined ? { attempt: meta.attempt } : {}),
|
|
219
|
+
...(meta.correlation ? { correlation: meta.correlation } : {}),
|
|
220
|
+
};
|
|
221
|
+
try {
|
|
222
|
+
const result = await handler(payload);
|
|
223
|
+
const rejected = result?.disposition === "rejected";
|
|
224
|
+
record({
|
|
225
|
+
...base,
|
|
226
|
+
statusCode: rejected ? 422 : 200,
|
|
227
|
+
durationMs: monotonicNowMs() - startedAt,
|
|
228
|
+
...(result?.eventType ? { eventType: result.eventType } : {}),
|
|
229
|
+
...(result?.correlation ? { correlation: result.correlation } : {}),
|
|
230
|
+
outcome: result?.outcome ?? { accepted: !rejected },
|
|
231
|
+
});
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
record({
|
|
236
|
+
...base,
|
|
237
|
+
statusCode: 500,
|
|
238
|
+
durationMs: monotonicNowMs() - startedAt,
|
|
239
|
+
outcome: { accepted: false },
|
|
240
|
+
});
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
record,
|
|
247
|
+
observeWebhook,
|
|
248
|
+
observeFetch,
|
|
249
|
+
observeQueuePublish,
|
|
250
|
+
observeQueueConsumer,
|
|
251
|
+
observeScheduledFeed,
|
|
252
|
+
async flush() {
|
|
253
|
+
await waitForInspections();
|
|
254
|
+
await shipper.flush();
|
|
255
|
+
},
|
|
256
|
+
async stop() {
|
|
257
|
+
await waitForInspections();
|
|
258
|
+
await shipper.stop();
|
|
259
|
+
},
|
|
260
|
+
stats: () => ({ ...shipper.stats(), buildErrors }),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function prepareRequestPayloadInspection(input, init, explicitPayload) {
|
|
264
|
+
if (explicitPayload !== undefined)
|
|
265
|
+
return Promise.resolve(explicitPayload);
|
|
266
|
+
if (typeof init?.body === "string") {
|
|
267
|
+
return Promise.resolve().then(() => {
|
|
268
|
+
try {
|
|
269
|
+
return JSON.parse(init.body);
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
if (typeof Request !== "undefined" && input instanceof Request) {
|
|
277
|
+
try {
|
|
278
|
+
const requestClone = input.clone();
|
|
279
|
+
return Promise.resolve().then(async () => {
|
|
280
|
+
try {
|
|
281
|
+
return (await requestClone.json());
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return Promise.resolve(undefined);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return Promise.resolve(undefined);
|
|
293
|
+
}
|
|
294
|
+
//# sourceMappingURL=observe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observe.js","sourceRoot":"","sources":["../src/observe.ts"],"names":[],"mappings":"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"}
|
package/dist/redact.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Json } from "@seamward/contracts";
|
|
2
|
+
/**
|
|
3
|
+
* Collector-side redaction - runs BEFORE anything leaves the customer process.
|
|
4
|
+
*
|
|
5
|
+
* Invariant (grep-provable across the codebase): raw payload bodies, credential
|
|
6
|
+
* headers, and configured sensitive fields never appear in an envelope. The
|
|
7
|
+
* collector transmits structure and keyed hashes, not content.
|
|
8
|
+
*/
|
|
9
|
+
/** Headers dropped unconditionally, whatever the policy says. */
|
|
10
|
+
export declare const ALWAYS_DROPPED_HEADERS: readonly ["authorization", "proxy-authorization", "cookie", "set-cookie", "x-api-key"];
|
|
11
|
+
export interface RedactionPolicy {
|
|
12
|
+
/** Policy identifier stamped into every envelope for auditability. */
|
|
13
|
+
version: string;
|
|
14
|
+
/** Non-secret key generation identifier. Derived from hashKey when omitted. */
|
|
15
|
+
hashNamespace?: string;
|
|
16
|
+
/** Case-insensitive field names whose values are dropped entirely. */
|
|
17
|
+
dropFields: string[];
|
|
18
|
+
/** Case-insensitive field names whose values are replaced by keyed hashes. */
|
|
19
|
+
hashFields: string[];
|
|
20
|
+
/** Key for hashing identifiers (NOT a secret shared with Seamward). */
|
|
21
|
+
hashKey: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function keyedHash(value: string, key: string): string;
|
|
24
|
+
/** Drop always-dropped + policy headers from a header map (case-insensitive). */
|
|
25
|
+
export declare function redactHeaders(headers: Record<string, string>, policy: RedactionPolicy): Record<string, string>;
|
|
26
|
+
/**
|
|
27
|
+
* Apply a redaction policy to a JSON payload, recursively:
|
|
28
|
+
* - dropFields are removed wherever they appear
|
|
29
|
+
* - hashFields are replaced with keyed hashes of their string form
|
|
30
|
+
*/
|
|
31
|
+
export declare function redactPayload(value: Json, policy: RedactionPolicy): Json;
|
package/dist/redact.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Collector-side redaction - runs BEFORE anything leaves the customer process.
|
|
4
|
+
*
|
|
5
|
+
* Invariant (grep-provable across the codebase): raw payload bodies, credential
|
|
6
|
+
* headers, and configured sensitive fields never appear in an envelope. The
|
|
7
|
+
* collector transmits structure and keyed hashes, not content.
|
|
8
|
+
*/
|
|
9
|
+
/** Headers dropped unconditionally, whatever the policy says. */
|
|
10
|
+
export const ALWAYS_DROPPED_HEADERS = [
|
|
11
|
+
"authorization",
|
|
12
|
+
"proxy-authorization",
|
|
13
|
+
"cookie",
|
|
14
|
+
"set-cookie",
|
|
15
|
+
"x-api-key",
|
|
16
|
+
];
|
|
17
|
+
export function keyedHash(value, key) {
|
|
18
|
+
return `sha256:${createHash("sha256").update(`${key}:${value}`, "utf8").digest("hex")}`;
|
|
19
|
+
}
|
|
20
|
+
/** Drop always-dropped + policy headers from a header map (case-insensitive). */
|
|
21
|
+
export function redactHeaders(headers, policy) {
|
|
22
|
+
const dropped = new Set([
|
|
23
|
+
...ALWAYS_DROPPED_HEADERS,
|
|
24
|
+
...policy.dropFields.map((f) => f.toLowerCase()),
|
|
25
|
+
]);
|
|
26
|
+
const out = {};
|
|
27
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
28
|
+
if (!dropped.has(k.toLowerCase()))
|
|
29
|
+
out[k] = v;
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Apply a redaction policy to a JSON payload, recursively:
|
|
35
|
+
* - dropFields are removed wherever they appear
|
|
36
|
+
* - hashFields are replaced with keyed hashes of their string form
|
|
37
|
+
*/
|
|
38
|
+
export function redactPayload(value, policy) {
|
|
39
|
+
const drop = new Set(policy.dropFields.map((f) => f.toLowerCase()));
|
|
40
|
+
const hash = new Set(policy.hashFields.map((f) => f.toLowerCase()));
|
|
41
|
+
const walk = (v) => {
|
|
42
|
+
if (v === null || typeof v !== "object")
|
|
43
|
+
return v;
|
|
44
|
+
if (Array.isArray(v))
|
|
45
|
+
return v.map(walk);
|
|
46
|
+
const out = {};
|
|
47
|
+
for (const [k, child] of Object.entries(v)) {
|
|
48
|
+
const key = k.toLowerCase();
|
|
49
|
+
if (drop.has(key))
|
|
50
|
+
continue;
|
|
51
|
+
if (hash.has(key)) {
|
|
52
|
+
out[k] = keyedHash(typeof child === "string" ? child : JSON.stringify(child), policy.hashKey);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
out[k] = walk(child);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
};
|
|
59
|
+
return walk(value);
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=redact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.js","sourceRoot":"","sources":["../src/redact.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAGzC;;;;;;GAMG;AAEH,iEAAiE;AACjE,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,eAAe;IACf,qBAAqB;IACrB,QAAQ;IACR,YAAY;IACZ,WAAW;CACH,CAAC;AAeX,MAAM,UAAU,SAAS,CAAC,KAAa,EAAE,GAAW;IAClD,OAAO,UAAU,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,GAAG,IAAI,KAAK,EAAE,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;AAC1F,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,aAAa,CAC3B,OAA+B,EAC/B,MAAuB;IAEvB,MAAM,OAAO,GAAG,IAAI,GAAG,CAAS;QAC9B,GAAG,sBAAsB;QACzB,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;KACjD,CAAC,CAAC;IACH,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC7C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAChD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,KAAW,EAAE,MAAuB;IAChE,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IACpE,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAEpE,MAAM,IAAI,GAAG,CAAC,CAAO,EAAQ,EAAE;QAC7B,IAAI,CAAC,KAAK,IAAI,IAAI,OAAO,CAAC,KAAK,QAAQ;YAAE,OAAO,CAAC,CAAC;QAClD,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;YAAE,OAAO,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACzC,MAAM,GAAG,GAA4B,EAAE,CAAC;QACxC,KAAK,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3C,MAAM,GAAG,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;YAC5B,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,SAAS;YAC5B,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;gBAClB,GAAG,CAAC,CAAC,CAAC,GAAG,SAAS,CAChB,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EACzD,MAAM,CAAC,OAAO,CACf,CAAC;gBACF,SAAS;YACX,CAAC;YACD,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAa,CAAC,CAAC;QAC/B,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC,CAAC;IAEF,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC;AACrB,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type ObservationEnvelope } from "@seamward/contracts";
|
|
2
|
+
/**
|
|
3
|
+
* Batched, signed, FAIL-OPEN envelope shipping.
|
|
4
|
+
*
|
|
5
|
+
* The collector runs inside the CUSTOMER'S process. The prime directive is
|
|
6
|
+
* that Seamward being slow, down, or misconfigured must never affect the
|
|
7
|
+
* host application:
|
|
8
|
+
* - enqueue() never throws and never blocks.
|
|
9
|
+
* - Shipping failures are counted and retried on the next flush - never
|
|
10
|
+
* surfaced as exceptions to the host.
|
|
11
|
+
* - Bounded memory: when the queue exceeds maxQueueSize, the OLDEST
|
|
12
|
+
* envelopes are dropped (drop-oldest) and counted. Losing telemetry is
|
|
13
|
+
* acceptable; growing the customer's heap is not.
|
|
14
|
+
*
|
|
15
|
+
* Batches are signed with the collector credential (t=,v1= HMAC scheme from
|
|
16
|
+
* @seamward/contracts) so ingest can authenticate and replay-protect.
|
|
17
|
+
*/
|
|
18
|
+
export interface ShipperConfig {
|
|
19
|
+
endpoint: string;
|
|
20
|
+
/** Current write-only ingestion authentication. */
|
|
21
|
+
sourceKey?: string;
|
|
22
|
+
ingestToken?: string;
|
|
23
|
+
/** Legacy connection-bundle authentication. */
|
|
24
|
+
credentialId?: string;
|
|
25
|
+
secret?: string;
|
|
26
|
+
maxBatchSize?: number;
|
|
27
|
+
flushIntervalMs?: number;
|
|
28
|
+
maxQueueSize?: number;
|
|
29
|
+
/** Injected for tests. Defaults: globalThis.fetch / Date.now. */
|
|
30
|
+
fetchFn?: typeof fetch;
|
|
31
|
+
nowSec?: () => number;
|
|
32
|
+
}
|
|
33
|
+
export interface ShipperStats {
|
|
34
|
+
enqueued: number;
|
|
35
|
+
shipped: number;
|
|
36
|
+
dropped: number;
|
|
37
|
+
failedBatches: number;
|
|
38
|
+
queueLength: number;
|
|
39
|
+
}
|
|
40
|
+
export interface Shipper {
|
|
41
|
+
enqueue(envelope: ObservationEnvelope): void;
|
|
42
|
+
/** Ship everything currently queued. Resolves when done; never rejects. */
|
|
43
|
+
flush(): Promise<void>;
|
|
44
|
+
/** Stop the interval timer and flush once. Never rejects. */
|
|
45
|
+
stop(): Promise<void>;
|
|
46
|
+
stats(): ShipperStats;
|
|
47
|
+
}
|
|
48
|
+
export declare function createShipper(config: ShipperConfig): Shipper;
|
package/dist/shipper.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { signBody } from "@seamward/contracts";
|
|
2
|
+
export function createShipper(config) {
|
|
3
|
+
const fetchFn = config.fetchFn ?? fetch;
|
|
4
|
+
const nowSec = config.nowSec ?? (() => Math.floor(Date.now() / 1000));
|
|
5
|
+
const maxBatchSize = config.maxBatchSize ?? 50;
|
|
6
|
+
const maxQueueSize = config.maxQueueSize ?? 1000;
|
|
7
|
+
const flushIntervalMs = config.flushIntervalMs ?? 5_000;
|
|
8
|
+
let queue = [];
|
|
9
|
+
const stats = { enqueued: 0, shipped: 0, dropped: 0, failedBatches: 0 };
|
|
10
|
+
// Serialise flushes: concurrent callers await the same chain, batches
|
|
11
|
+
// never interleave, and a rejection can never escape (fail-open).
|
|
12
|
+
let chain = Promise.resolve();
|
|
13
|
+
const timer = setInterval(() => {
|
|
14
|
+
void flush();
|
|
15
|
+
}, flushIntervalMs);
|
|
16
|
+
// Never keep the customer's process alive just for telemetry.
|
|
17
|
+
timer.unref?.();
|
|
18
|
+
async function shipQueued() {
|
|
19
|
+
while (queue.length > 0) {
|
|
20
|
+
const batch = queue.slice(0, maxBatchSize);
|
|
21
|
+
const body = JSON.stringify({ envelopes: batch });
|
|
22
|
+
try {
|
|
23
|
+
const signingSecret = config.ingestToken ?? config.secret;
|
|
24
|
+
if (!signingSecret)
|
|
25
|
+
throw new Error("collector authentication is required");
|
|
26
|
+
const response = await fetchFn(config.endpoint, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: {
|
|
29
|
+
"content-type": "application/json",
|
|
30
|
+
...(config.sourceKey && config.ingestToken
|
|
31
|
+
? {
|
|
32
|
+
authorization: `Bearer ${config.ingestToken}`,
|
|
33
|
+
"x-seamward-source": config.sourceKey,
|
|
34
|
+
}
|
|
35
|
+
: config.credentialId
|
|
36
|
+
? { "x-seamward-credential": config.credentialId }
|
|
37
|
+
: {}),
|
|
38
|
+
"x-seamward-signature": signBody(body, signingSecret, nowSec()),
|
|
39
|
+
},
|
|
40
|
+
body,
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
stats.failedBatches += 1;
|
|
44
|
+
return; // keep the batch queued; retry on next flush
|
|
45
|
+
}
|
|
46
|
+
queue = queue.slice(batch.length);
|
|
47
|
+
stats.shipped += batch.length;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
stats.failedBatches += 1;
|
|
51
|
+
return; // fail-open: never throw into the host app
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function flush() {
|
|
56
|
+
chain = chain.then(shipQueued).catch(() => { });
|
|
57
|
+
return chain;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
enqueue(envelope) {
|
|
61
|
+
stats.enqueued += 1;
|
|
62
|
+
queue.push(envelope);
|
|
63
|
+
const overflow = queue.length - maxQueueSize;
|
|
64
|
+
if (overflow > 0) {
|
|
65
|
+
queue.splice(0, overflow);
|
|
66
|
+
stats.dropped += overflow;
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
flush,
|
|
70
|
+
async stop() {
|
|
71
|
+
clearInterval(timer);
|
|
72
|
+
await flush();
|
|
73
|
+
},
|
|
74
|
+
stats() {
|
|
75
|
+
return { ...stats, queueLength: queue.length };
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=shipper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shipper.js","sourceRoot":"","sources":["../src/shipper.ts"],"names":[],"mappings":"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"}
|