@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 ADDED
@@ -0,0 +1,307 @@
1
+ # Seamward Node collector
2
+
3
+ The Seamward collector observes third-party HTTP API, webhook, queue, and
4
+ scheduled-feed behaviour from a Node.js backend. It batches redacted evidence
5
+ asynchronously and never sits on the application's critical request path.
6
+
7
+ This `0.1.0-alpha` release line is for pilot evaluation with Node.js 22 or newer.
8
+ TypeScript and server-side JavaScript are supported. PHP, Go, browser, and edge
9
+ runtime collectors are not shipped yet.
10
+
11
+ ## 1. Create the Seamward connection
12
+
13
+ In Seamward:
14
+
15
+ 1. Open the workspace and create an application environment under **Settings**.
16
+ 2. Generate its ingest token and copy it immediately. The token is shown once.
17
+ 3. Create or open an integration.
18
+ 4. Copy the public Connection key from the integration's **Collector setup** tab.
19
+
20
+ The Connection key contains public routing identifiers and grants no access by
21
+ itself. The ingest token is a write-only secret: keep it in the backend secret
22
+ manager and never put it in browser code, Git, logs, screenshots, or support
23
+ messages.
24
+
25
+ ## 2. Install the pilot release
26
+
27
+ ```bash
28
+ pnpm add @seamward/collector
29
+ ```
30
+
31
+ or `npm install @seamward/collector`. This is a prerelease; pin the version
32
+ through your lockfile as with any dependency.
33
+
34
+ ## 3. Configure the backend
35
+
36
+ ```ini
37
+ SEAMWARD_WEBHOOK_CONNECTION_KEY=sw_conn_v1.replace_webhook
38
+ SEAMWARD_HTTP_API_CONNECTION_KEY=sw_conn_v1.replace_http_api
39
+ SEAMWARD_QUEUE_PUBLISH_CONNECTION_KEY=sw_conn_v1.replace_queue_publish
40
+ SEAMWARD_QUEUE_CONSUME_CONNECTION_KEY=sw_conn_v1.replace_queue_consume
41
+ SEAMWARD_FEED_EXPORT_CONNECTION_KEY=sw_conn_v1.replace_feed_export
42
+ SEAMWARD_INGEST_TOKEN=sw_ing_replace_me
43
+ ```
44
+
45
+ `SEAMWARD_INGEST_URL` is optional and only needed for a self-hosted Seamward
46
+ deployment. For example, use `https://seamward.example/ingest`. The collector
47
+ otherwise uses `https://api.seamward.com/ingest`.
48
+
49
+ Create one collector per Integration boundary. An Integration has one direction
50
+ and one protocol. Processes that observe multiple boundaries create a collector
51
+ for each Connection key and may share the application environment's ingest
52
+ token:
53
+
54
+ ```ts
55
+ import { createSeamwardCollector } from "@seamward/collector";
56
+
57
+ const connected = (connectionKey: string) =>
58
+ createSeamwardCollector({
59
+ connectionKey,
60
+ ingestToken: process.env.SEAMWARD_INGEST_TOKEN!,
61
+ endpoint: process.env.SEAMWARD_INGEST_URL,
62
+ deployment: {
63
+ service: "candidate-api",
64
+ release: process.env.RELEASE_VERSION,
65
+ commitSha: process.env.GITHUB_SHA,
66
+ },
67
+ });
68
+
69
+ export const webhookCollector = connected(
70
+ process.env.SEAMWARD_WEBHOOK_CONNECTION_KEY!,
71
+ );
72
+ export const httpApiCollector = connected(
73
+ process.env.SEAMWARD_HTTP_API_CONNECTION_KEY!,
74
+ );
75
+ export const queuePublisherCollector = connected(
76
+ process.env.SEAMWARD_QUEUE_PUBLISH_CONNECTION_KEY!,
77
+ );
78
+ export const queueConsumerCollector = connected(
79
+ process.env.SEAMWARD_QUEUE_CONSUME_CONNECTION_KEY!,
80
+ );
81
+ export const feedExportCollector = connected(
82
+ process.env.SEAMWARD_FEED_EXPORT_CONNECTION_KEY!,
83
+ );
84
+ ```
85
+
86
+ Release and commit metadata are optional. The collector also recognises
87
+ `SEAMWARD_COMMIT_SHA`, `VERCEL_GIT_COMMIT_SHA`, `RENDER_GIT_COMMIT`,
88
+ `RAILWAY_GIT_COMMIT_SHA`, and `GITHUB_SHA`. Invalid values are ignored instead
89
+ of breaking the host application.
90
+
91
+ ## 4. Observe an inbound webhook
92
+
93
+ Wrap the existing business handler; do not send the inbound request headers to
94
+ Seamward.
95
+
96
+ ```ts
97
+ const handleCandidateWebhook = webhookCollector.observeWebhook(
98
+ {
99
+ routeTemplate: "/webhooks/candidates",
100
+ },
101
+ async (payload) => {
102
+ const candidate = await candidateService.accept(payload);
103
+
104
+ return {
105
+ statusCode: 202,
106
+ eventType: "candidate.create",
107
+ outcome: {
108
+ accepted: true,
109
+ businessObjectType: "candidate",
110
+ businessObjectId: candidate.id,
111
+ },
112
+ correlation: { sourceEventId: candidate.providerEventId },
113
+ };
114
+ },
115
+ );
116
+
117
+ fastify.post("/webhooks/candidates", async (request, reply) => {
118
+ const result = await handleCandidateWebhook(
119
+ request.body as Record<string, unknown>,
120
+ );
121
+ return reply.code(result.statusCode ?? 200).send();
122
+ });
123
+ ```
124
+
125
+ Use stable route templates such as `/webhooks/candidates`, never concrete IDs,
126
+ query strings, email addresses, or other high-cardinality values.
127
+
128
+ ## 5. Observe an outbound API call
129
+
130
+ ```ts
131
+ const providerFetch = httpApiCollector.observeFetch({
132
+ routeTemplate: "/v1/candidates",
133
+ eventType: "candidate.create",
134
+ payloadLocation: "response",
135
+ });
136
+
137
+ const response = await providerFetch("https://provider.example/v1/candidates", {
138
+ method: "POST",
139
+ headers: {
140
+ authorization: `Bearer ${process.env.PROVIDER_TOKEN}`,
141
+ "content-type": "application/json",
142
+ },
143
+ body: JSON.stringify(candidate),
144
+ });
145
+ ```
146
+
147
+ The collector never receives the request headers. By default, it clones the
148
+ response and inspects JSON asynchronously, so consuming the original response
149
+ is not delayed. Set `payloadLocation: "request"` to observe the JSON request
150
+ shape instead. String bodies and cloned `Request` bodies are inspected safely.
151
+ For another body type, pass a local-only `requestPayload`; its values remain in
152
+ the process and only its redacted structural shape enters the envelope.
153
+
154
+ ## 6. Observe a queue boundary
155
+
156
+ Use the broker-neutral wrappers around the publish and consume functions you
157
+ already own. Seamward does not acknowledge, retry, or otherwise control the
158
+ broker operation.
159
+
160
+ ```ts
161
+ const publishCandidate = queuePublisherCollector.observeQueuePublish(
162
+ {
163
+ queueName: "candidate-events",
164
+ eventType: "candidate.published",
165
+ },
166
+ async (message) => {
167
+ await broker.publish("candidate-events", message);
168
+ return { disposition: "acknowledged" as const };
169
+ },
170
+ );
171
+
172
+ const consumeCandidate = queueConsumerCollector.observeQueueConsumer(
173
+ {
174
+ queueName: "candidate-events",
175
+ eventType: "candidate.received",
176
+ },
177
+ async (message) => {
178
+ await candidateService.accept(message);
179
+ return { disposition: "acknowledged" as const };
180
+ },
181
+ );
182
+ ```
183
+
184
+ Use a stable queue, topic, or subscription label. Never use a message ID.
185
+ Broker-specific adapters are not shipped in this pilot.
186
+
187
+ ## 7. Observe a scheduled feed
188
+
189
+ Wrap one scheduled import or export run. The wrapper records completion,
190
+ rejection, duration, attempt, and correlation evidence without changing the
191
+ job's return value or error behaviour.
192
+
193
+ ```ts
194
+ const exportCandidates = feedExportCollector.observeScheduledFeed(
195
+ {
196
+ feedName: "nightly-candidate-export",
197
+ direction: "outbound",
198
+ eventType: "candidate.exported",
199
+ },
200
+ async (payload) => {
201
+ await provider.upload(payload);
202
+ return { disposition: "completed" as const };
203
+ },
204
+ );
205
+ ```
206
+
207
+ The v0.2 wire envelope retains `method`, `routeTemplate`, and `statusCode` for
208
+ backward compatibility. Queue and feed wrappers project their native concepts
209
+ into those fields, so application code does not supply synthetic HTTP values.
210
+
211
+ ## 8. Redaction and correlation
212
+
213
+ Payload values and headers are never included in the observation envelope. The
214
+ collector derives only the structural shape (`string`, `number`, object fields,
215
+ and so on) and its fingerprint before the envelope leaves the process. Field
216
+ names remain visible because they are required for contract-drift detection.
217
+
218
+ The default policy identifies these common secret-bearing fields:
219
+
220
+ ```text
221
+ password, access_token, refresh_token, authorization, cookie, set-cookie
222
+ ```
223
+
224
+ Give an application-specific policy version so evidence can be traced to the
225
+ configuration that produced it:
226
+
227
+ ```ts
228
+ const seamward = createSeamwardCollector({
229
+ connectionKey: process.env.SEAMWARD_CONNECTION_KEY!,
230
+ ingestToken: process.env.SEAMWARD_INGEST_TOKEN!,
231
+ policy: {
232
+ version: "candidate-api-v1",
233
+ dropFields: [
234
+ "password",
235
+ "access_token",
236
+ "refresh_token",
237
+ "authorization",
238
+ "cookie",
239
+ "set-cookie",
240
+ "full_name",
241
+ "email",
242
+ "phone_number",
243
+ ],
244
+ hashFields: [],
245
+ },
246
+ });
247
+ ```
248
+
249
+ Do not place identifiers in `eventType`, `routeTemplate`, release labels, or
250
+ service names. Put only identifiers needed for reconciliation into
251
+ `correlation.sourceEventId`, `correlation.idempotencyKey`, or
252
+ `outcome.businessObjectId`. The collector converts those values to keyed
253
+ SHA-256 hashes locally; Seamward receives no original identifier.
254
+
255
+ ## 9. Shutdown and health
256
+
257
+ Flush before a controlled process shutdown:
258
+
259
+ ```ts
260
+ process.once("SIGTERM", async () => {
261
+ await Promise.all([
262
+ webhookCollector.stop(),
263
+ httpApiCollector.stop(),
264
+ queuePublisherCollector.stop(),
265
+ queueConsumerCollector.stop(),
266
+ feedExportCollector.stop(),
267
+ ]);
268
+ process.exit(0);
269
+ });
270
+ ```
271
+
272
+ `stop()` clears the timer and attempts one final flush. `flush()` and `stop()`
273
+ never reject because Seamward telemetry must not break the host application.
274
+ Inspect bounded operational counters without logging observations or tokens:
275
+
276
+ ```ts
277
+ const { enqueued, shipped, dropped, failedBatches, queueLength, buildErrors } =
278
+ httpApiCollector.stats();
279
+ ```
280
+
281
+ Defaults:
282
+
283
+ - flush interval: 5 seconds;
284
+ - batch size: 50 observations;
285
+ - in-memory queue: 1,000 observations;
286
+ - overflow policy: drop oldest and increment `dropped`;
287
+ - failed request policy: retain the batch and retry on the next flush;
288
+ - application errors: preserve the application's original error behaviour.
289
+
290
+ ## 10. Verify the connection
291
+
292
+ 1. Start the backend with the Connection key and ingest token configured.
293
+ 2. Exercise one synthetic HTTP request, webhook, queue message, or feed run.
294
+ 3. Wait for the five-second automatic flush, or call `await httpApiCollector.flush()` for the collector you exercised.
295
+ 4. Return to the integration in Seamward and select **Check for observation**.
296
+ 5. Confirm the first observation time and deployment metadata.
297
+
298
+ If the integration remains unobserved:
299
+
300
+ - confirm the collector is running in the backend rather than the browser;
301
+ - copy the Connection key again from the integration you are testing;
302
+ - confirm the ingest token belongs to that integration's application environment;
303
+ - inspect the relevant collector's `stats()` result for `failedBatches`, `dropped`, or `buildErrors`;
304
+ - verify the local API is reachable at the configured ingest URL;
305
+ - generate a new ingest token if the original was not saved or may be exposed.
306
+
307
+ Do not disable redaction or log request bodies to troubleshoot the connection.
@@ -0,0 +1,49 @@
1
+ import { type Json } from "@seamward/contracts";
2
+ import { type Collector, type CollectorConfig, type FetchMeta, type ObservedFetch, type QueueMeta, type QueueResult, type RecordInput, type ScheduledFeedMeta, type ScheduledFeedResult, type WebhookMeta, type WebhookResult } from "./observe.js";
3
+ import type { RedactionPolicy } from "./redact.js";
4
+ export interface ConnectOptions extends Pick<CollectorConfig, "deployment" | "builderDeps" | "maxBatchSize" | "flushIntervalMs" | "maxQueueSize" | "fetchFn" | "nowSec"> {
5
+ /** Override for self-hosted and local Seamward ingestion. */
6
+ endpoint?: string;
7
+ policy?: Omit<RedactionPolicy, "hashKey">;
8
+ }
9
+ export type CreateSeamwardCollectorConfig = ConnectOptions & {
10
+ /** Write-only server credential. Store this in an environment variable. */
11
+ ingestToken: string;
12
+ } & ({
13
+ /** Public key that binds one source and integration. Grants no access by itself. */
14
+ connectionKey: string;
15
+ sourceKey?: never;
16
+ } | {
17
+ /** Legacy public application/environment identifier. */
18
+ sourceKey: string;
19
+ connectionKey?: never;
20
+ });
21
+ export type ConnectedRecordInput = Omit<RecordInput, "integrationId"> & {
22
+ integrationKey?: string;
23
+ };
24
+ export type ConnectedWebhookMeta = Omit<WebhookMeta, "integrationId"> & {
25
+ integrationKey?: string;
26
+ };
27
+ export type ConnectedFetchMeta = Omit<FetchMeta, "integrationId"> & {
28
+ integrationKey?: string;
29
+ };
30
+ export type ConnectedQueueMeta = Omit<QueueMeta, "integrationId"> & {
31
+ integrationKey?: string;
32
+ };
33
+ export type ConnectedScheduledFeedMeta = Omit<ScheduledFeedMeta, "integrationId"> & {
34
+ integrationKey?: string;
35
+ };
36
+ export interface ConnectedCollector extends Omit<Collector, "record" | "observeWebhook" | "observeFetch"> {
37
+ record(input: ConnectedRecordInput): void;
38
+ observeWebhook<R extends WebhookResult | void>(meta: ConnectedWebhookMeta, handler: (payload: Json) => R | Promise<R>): (payload: Json) => Promise<R>;
39
+ observeFetch(meta: ConnectedFetchMeta, fetchFn?: typeof fetch): ObservedFetch;
40
+ observeQueuePublish<R extends QueueResult | void>(meta: ConnectedQueueMeta, publisher: (message: Json) => R | Promise<R>): (message: Json) => Promise<R>;
41
+ observeQueueConsumer<R extends QueueResult | void>(meta: ConnectedQueueMeta, consumer: (message: Json) => R | Promise<R>): (message: Json) => Promise<R>;
42
+ observeScheduledFeed<R extends ScheduledFeedResult | void>(meta: ConnectedScheduledFeedMeta, handler: (payload: Json) => R | Promise<R>): (payload: Json) => Promise<R>;
43
+ }
44
+ /**
45
+ * Backend collector setup. The public Source key identifies the deployed
46
+ * application; Integration keys route its observations, and the write-only
47
+ * ingest token authenticates and signs every batch.
48
+ */
49
+ export declare function createSeamwardCollector(config: CreateSeamwardCollectorConfig): ConnectedCollector;
@@ -0,0 +1,84 @@
1
+ import { createHash } from "node:crypto";
2
+ import { parseIngestToken, parseConnectionKey, parseIntegrationKey, parseSourceKey, } from "@seamward/contracts";
3
+ import { createCollector, } from "./observe.js";
4
+ const defaultPolicy = {
5
+ version: "seamward-default-v1",
6
+ dropFields: [
7
+ "password",
8
+ "access_token",
9
+ "refresh_token",
10
+ "authorization",
11
+ "cookie",
12
+ "set-cookie",
13
+ ],
14
+ hashFields: [],
15
+ };
16
+ function defaultIngestEndpoint() {
17
+ return process.env.SEAMWARD_INGEST_URL ?? "https://api.seamward.com/ingest";
18
+ }
19
+ /**
20
+ * Backend collector setup. The public Source key identifies the deployed
21
+ * application; Integration keys route its observations, and the write-only
22
+ * ingest token authenticates and signs every batch.
23
+ */
24
+ export function createSeamwardCollector(config) {
25
+ const { connectionKey: rawConnectionKey, ingestToken: rawIngestToken, endpoint = defaultIngestEndpoint(), policy = defaultPolicy, sourceKey: rawSourceKey, ...options } = config;
26
+ let sourceKey;
27
+ let boundIntegrationKey = null;
28
+ let ingestToken;
29
+ try {
30
+ if (rawConnectionKey) {
31
+ const parsed = parseConnectionKey(rawConnectionKey);
32
+ sourceKey = parsed.sourceKey;
33
+ boundIntegrationKey = parsed.integrationKey;
34
+ }
35
+ else {
36
+ sourceKey = parseSourceKey(rawSourceKey ?? "");
37
+ }
38
+ }
39
+ catch {
40
+ throw new Error("Seamward connection key or source key is invalid");
41
+ }
42
+ try {
43
+ ingestToken = parseIngestToken(rawIngestToken);
44
+ }
45
+ catch {
46
+ throw new Error("Seamward ingest token is invalid");
47
+ }
48
+ const collector = createCollector({
49
+ endpoint,
50
+ sourceKey,
51
+ ingestToken,
52
+ // These placeholders satisfy the local strict envelope contract. The API
53
+ // replaces them with the token-bound scope before validation/persistence.
54
+ tenantId: "ten_authenticated",
55
+ environmentId: "env_authenticated",
56
+ policy: {
57
+ ...policy,
58
+ hashKey: createHash("sha256").update(ingestToken).digest("base64url"),
59
+ },
60
+ ...options,
61
+ });
62
+ function integrationId(value) {
63
+ const selected = value ?? boundIntegrationKey;
64
+ if (!selected) {
65
+ throw new Error("Seamward integration key is required when using a legacy source key");
66
+ }
67
+ return parseIntegrationKey(selected);
68
+ }
69
+ return {
70
+ record: ({ integrationKey, ...input }) => collector.record({
71
+ ...input,
72
+ integrationId: integrationId(integrationKey),
73
+ }),
74
+ observeWebhook: ({ integrationKey, ...meta }, handler) => collector.observeWebhook({ ...meta, integrationId: integrationId(integrationKey) }, handler),
75
+ observeFetch: ({ integrationKey, ...meta }, fetchFn) => collector.observeFetch({ ...meta, integrationId: integrationId(integrationKey) }, fetchFn),
76
+ observeQueuePublish: ({ integrationKey, ...meta }, publisher) => collector.observeQueuePublish({ ...meta, integrationId: integrationId(integrationKey) }, publisher),
77
+ observeQueueConsumer: ({ integrationKey, ...meta }, consumer) => collector.observeQueueConsumer({ ...meta, integrationId: integrationId(integrationKey) }, consumer),
78
+ observeScheduledFeed: ({ integrationKey, ...meta }, handler) => collector.observeScheduledFeed({ ...meta, integrationId: integrationId(integrationKey) }, handler),
79
+ flush: () => collector.flush(),
80
+ stop: () => collector.stop(),
81
+ stats: () => collector.stats(),
82
+ };
83
+ }
84
+ //# sourceMappingURL=connect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect.js","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import { type DeploymentContext } from "@seamward/contracts";
2
+ export type DeploymentEnvironment = Readonly<Record<string, string | undefined>>;
3
+ /**
4
+ * Resolve bounded, low-cardinality deployment evidence without making it a
5
+ * collector requirement. Explicit SDK values win; known runtime variables are
6
+ * fallbacks. Invalid values are ignored rather than breaking host telemetry.
7
+ */
8
+ export declare function resolveDeploymentContext(explicit?: Partial<DeploymentContext>, environment?: DeploymentEnvironment): DeploymentContext | undefined;
@@ -0,0 +1,43 @@
1
+ import { deploymentContextSchema, } from "@seamward/contracts";
2
+ const commitEnvironmentKeys = [
3
+ "SEAMWARD_COMMIT_SHA",
4
+ "VERCEL_GIT_COMMIT_SHA",
5
+ "RENDER_GIT_COMMIT",
6
+ "RAILWAY_GIT_COMMIT_SHA",
7
+ "GITHUB_SHA",
8
+ ];
9
+ function validField(key, candidates) {
10
+ for (const candidate of candidates) {
11
+ if (candidate === undefined || candidate.length === 0)
12
+ continue;
13
+ const parsed = deploymentContextSchema.safeParse({ [key]: candidate });
14
+ if (parsed.success)
15
+ return parsed.data[key];
16
+ }
17
+ return undefined;
18
+ }
19
+ /**
20
+ * Resolve bounded, low-cardinality deployment evidence without making it a
21
+ * collector requirement. Explicit SDK values win; known runtime variables are
22
+ * fallbacks. Invalid values are ignored rather than breaking host telemetry.
23
+ */
24
+ export function resolveDeploymentContext(explicit = {}, environment = process.env) {
25
+ const service = validField("service", [explicit.service, environment.SEAMWARD_SERVICE]);
26
+ const commitSha = validField("commitSha", [
27
+ explicit.commitSha?.toLowerCase(),
28
+ ...commitEnvironmentKeys.map((key) => environment[key]?.toLowerCase()),
29
+ ]);
30
+ const release = validField("release", [
31
+ explicit.release,
32
+ environment.SEAMWARD_RELEASE,
33
+ commitSha?.slice(0, 12),
34
+ ]);
35
+ const context = {
36
+ ...(service ? { service } : {}),
37
+ ...(release ? { release } : {}),
38
+ ...(commitSha ? { commitSha } : {}),
39
+ };
40
+ const parsed = deploymentContextSchema.safeParse(context);
41
+ return parsed.success ? parsed.data : undefined;
42
+ }
43
+ //# sourceMappingURL=deployment-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deployment-context.js","sourceRoot":"","sources":["../src/deployment-context.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,GAExB,MAAM,qBAAqB,CAAC;AAI7B,MAAM,qBAAqB,GAAG;IAC5B,qBAAqB;IACrB,uBAAuB;IACvB,mBAAmB;IACnB,wBAAwB;IACxB,YAAY;CACJ,CAAC;AAEX,SAAS,UAAU,CACjB,GAAM,EACN,UAAqC;IAErC,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,SAAS,KAAK,SAAS,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAChE,MAAM,MAAM,GAAG,uBAAuB,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC;QACvE,IAAI,MAAM,CAAC,OAAO;YAAE,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC9C,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CACtC,WAAuC,EAAE,EACzC,cAAqC,OAAO,CAAC,GAAG;IAEhD,MAAM,OAAO,GAAG,UAAU,CAAC,SAAS,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,gBAAgB,CAAC,CAAC,CAAC;IACxF,MAAM,SAAS,GAAG,UAAU,CAAC,WAAW,EAAE;QACxC,QAAQ,CAAC,SAAS,EAAE,WAAW,EAAE;QACjC,GAAG,qBAAqB,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,WAAW,EAAE,CAAC;KACvE,CAAC,CAAC;IACH,MAAM,OAAO,GAAG,UAAU,CAAC,SAAS,EAAE;QACpC,QAAQ,CAAC,OAAO;QAChB,WAAW,CAAC,gBAAgB;QAC5B,SAAS,EAAE,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;KACxB,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG;QACd,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/B,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/B,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACpC,CAAC;IACF,MAAM,MAAM,GAAG,uBAAuB,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IAC1D,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AAClD,CAAC"}
@@ -0,0 +1,51 @@
1
+ import { type DeploymentContext, type Json, type ObservationEnvelope } from "@seamward/contracts";
2
+ import { type RedactionPolicy } from "./redact.js";
3
+ /**
4
+ * Builds a valid ObservationEnvelope from one observed request/response.
5
+ *
6
+ * Privacy order of operations (the load-bearing design):
7
+ * 1. The structural fingerprint is computed from the RAW payload - signatures
8
+ * are shape-only (field names + types, never values), so this is safe and
9
+ * keeps drift detection faithful to what the provider actually sent, even
10
+ * for fields the policy would drop from any stored content.
11
+ * 2. Correlation identifiers leave the process only as keyed hashes.
12
+ * 3. The envelope schema is strict and has NO field for headers or bodies:
13
+ * a leak would require changing @seamward/contracts, not a builder bug.
14
+ *
15
+ * Pure: clock and id generation are injected for determinism. The built
16
+ * envelope is validated through the strict schema before it is returned.
17
+ */
18
+ export interface ObservationInput {
19
+ tenantId: string;
20
+ environmentId: string;
21
+ integrationId: string;
22
+ direction: "inbound" | "outbound";
23
+ protocol: "http-webhook" | "http-api" | "scheduled-feed" | "queue";
24
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
25
+ routeTemplate: string;
26
+ payloadLocation?: "request" | "response" | "message";
27
+ statusCode: number;
28
+ durationMs: number;
29
+ attempt?: number;
30
+ /** Low-cardinality provider event label; never put identifiers or PII here. */
31
+ eventType?: string;
32
+ /** Raw payload - used for shape-only fingerprinting; never shipped. */
33
+ payload?: Json;
34
+ declaredContractVersion?: string;
35
+ deployment?: DeploymentContext;
36
+ correlation?: {
37
+ traceId?: string;
38
+ sourceEventId?: string;
39
+ idempotencyKey?: string;
40
+ };
41
+ outcome?: {
42
+ accepted: boolean;
43
+ businessObjectType?: string;
44
+ businessObjectId?: string;
45
+ };
46
+ }
47
+ export interface BuilderDeps {
48
+ now?: () => Date;
49
+ newId?: () => string;
50
+ }
51
+ export declare function buildEnvelope(input: ObservationInput, policy: RedactionPolicy, deps?: BuilderDeps): ObservationEnvelope;
@@ -0,0 +1,72 @@
1
+ import { createHmac, randomUUID } from "node:crypto";
2
+ import { ENVELOPE_VERSION, OPERATION_IDENTITY_VERSION, defaultPayloadLocation, parseEnvelope, schemaFingerprint, shapeOf, } from "@seamward/contracts";
3
+ import { keyedHash } from "./redact.js";
4
+ export function buildEnvelope(input, policy, deps = {}) {
5
+ const now = deps.now ?? (() => new Date());
6
+ const newId = deps.newId ?? (() => randomUUID().replaceAll("-", ""));
7
+ const fingerprint = schemaFingerprint(input.payload ?? null);
8
+ const correlation = {};
9
+ if (input.correlation?.traceId)
10
+ correlation.traceId = input.correlation.traceId;
11
+ if (input.correlation?.sourceEventId) {
12
+ correlation.sourceEventIdHash = keyedHash(input.correlation.sourceEventId, policy.hashKey);
13
+ }
14
+ if (input.correlation?.idempotencyKey) {
15
+ correlation.idempotencyKeyHash = keyedHash(input.correlation.idempotencyKey, policy.hashKey);
16
+ }
17
+ correlation.hashNamespace =
18
+ policy.hashNamespace ??
19
+ `key:${createHmac("sha256", policy.hashKey)
20
+ .update("seamward-hash-namespace-v1", "utf8")
21
+ .digest("hex")
22
+ .slice(0, 32)}`;
23
+ const outcome = input.outcome
24
+ ? {
25
+ accepted: input.outcome.accepted,
26
+ ...(input.outcome.businessObjectType
27
+ ? { businessObjectType: input.outcome.businessObjectType }
28
+ : {}),
29
+ ...(input.outcome.businessObjectId
30
+ ? {
31
+ businessObjectIdHash: keyedHash(input.outcome.businessObjectId, policy.hashKey),
32
+ }
33
+ : {}),
34
+ }
35
+ : { accepted: input.statusCode !== 0 && input.statusCode < 400 };
36
+ return parseEnvelope({
37
+ envelopeVersion: ENVELOPE_VERSION,
38
+ eventId: `evt_${newId()}`,
39
+ tenantId: input.tenantId,
40
+ environmentId: input.environmentId,
41
+ integrationId: input.integrationId,
42
+ direction: input.direction,
43
+ protocol: input.protocol,
44
+ occurredAt: now().toISOString(),
45
+ operationIdentityVersion: OPERATION_IDENTITY_VERSION,
46
+ ...(input.deployment ? { deployment: input.deployment } : {}),
47
+ ...(input.eventType ? { eventType: input.eventType } : {}),
48
+ correlation,
49
+ contract: {
50
+ ...(input.declaredContractVersion
51
+ ? { declaredVersion: input.declaredContractVersion }
52
+ : {}),
53
+ observedFingerprint: fingerprint,
54
+ },
55
+ transport: {
56
+ method: input.method,
57
+ routeTemplate: input.routeTemplate,
58
+ payloadLocation: input.payloadLocation ?? defaultPayloadLocation(input),
59
+ statusCode: input.statusCode,
60
+ durationMs: input.durationMs,
61
+ attempt: input.attempt ?? 1,
62
+ },
63
+ payload: {
64
+ storage: "none",
65
+ schemaFingerprint: fingerprint,
66
+ schemaShape: shapeOf(input.payload ?? null),
67
+ redactionPolicyVersion: policy.version,
68
+ },
69
+ outcome,
70
+ });
71
+ }
72
+ //# sourceMappingURL=envelope-builder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelope-builder.js","sourceRoot":"","sources":["../src/envelope-builder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export { redactPayload, redactHeaders, keyedHash, ALWAYS_DROPPED_HEADERS, } from "./redact.js";
2
+ export type { RedactionPolicy } from "./redact.js";
3
+ export { buildEnvelope } from "./envelope-builder.js";
4
+ export type { ObservationInput, BuilderDeps } from "./envelope-builder.js";
5
+ export { createShipper } from "./shipper.js";
6
+ export type { Shipper, ShipperConfig, ShipperStats } from "./shipper.js";
7
+ export { createCollector } from "./observe.js";
8
+ export type { Collector, CollectorConfig, CollectorStats, FetchMeta, ObservedFetch, QueueMeta, QueueResult, RecordInput, ScheduledFeedMeta, ScheduledFeedResult, WebhookMeta, WebhookResult, } from "./observe.js";
9
+ export { resolveDeploymentContext } from "./deployment-context.js";
10
+ export type { DeploymentEnvironment } from "./deployment-context.js";
11
+ export { createSeamwardCollector } from "./connect.js";
12
+ export type { CreateSeamwardCollectorConfig, ConnectedCollector, ConnectedFetchMeta, ConnectedQueueMeta, ConnectedRecordInput, ConnectedScheduledFeedMeta, ConnectedWebhookMeta, ConnectOptions, } from "./connect.js";
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { redactPayload, redactHeaders, keyedHash, ALWAYS_DROPPED_HEADERS, } from "./redact.js";
2
+ export { buildEnvelope } from "./envelope-builder.js";
3
+ export { createShipper } from "./shipper.js";
4
+ export { createCollector } from "./observe.js";
5
+ export { resolveDeploymentContext } from "./deployment-context.js";
6
+ export { createSeamwardCollector } from "./connect.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,aAAa,EACb,SAAS,EACT,sBAAsB,GACvB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAGtD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAG7C,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAgB/C,OAAO,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AAGnE,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC"}