@useairfoil/connector-kit 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/METRICS.md +78 -0
  2. package/README.md +450 -88
  3. package/dist/auth/index.d.ts +2 -0
  4. package/dist/auth/index.js +3 -0
  5. package/dist/auth-DvU_CZ7C.js +42 -0
  6. package/dist/auth-DvU_CZ7C.js.map +1 -0
  7. package/dist/chunk-Bp6m_JJh.js +13 -0
  8. package/dist/connector-app/index.d.ts +14 -0
  9. package/dist/connector-app/index.js +11 -0
  10. package/dist/connector-app-d7Z-SZLQ.js +64 -0
  11. package/dist/connector-app-d7Z-SZLQ.js.map +1 -0
  12. package/dist/constants-BRhIX_rz.js +27 -0
  13. package/dist/constants-BRhIX_rz.js.map +1 -0
  14. package/dist/errors/index.d.ts +2 -0
  15. package/dist/errors/index.js +3 -0
  16. package/dist/errors-DHWylqRZ.js +8 -0
  17. package/dist/errors-DHWylqRZ.js.map +1 -0
  18. package/dist/index-Byr0Lf0C.d.ts +25 -0
  19. package/dist/index-CBT-JX-T.d.ts +173 -0
  20. package/dist/index-CTHVudvQ.d.ts +41 -0
  21. package/dist/index-CTLYQ74P.d.ts +80 -0
  22. package/dist/index-Coh_SkBs.d.ts +95 -0
  23. package/dist/index-D54ySAKn.d.ts +14 -0
  24. package/dist/index-DtJX_tic.d.ts +153 -0
  25. package/dist/index-J42AZaXU.d.ts +38 -0
  26. package/dist/index-fIIBVAai.d.ts +25 -0
  27. package/dist/index-o1-Z3n7V.d.ts +55 -0
  28. package/dist/index.d.ts +14 -165
  29. package/dist/index.js +50 -228
  30. package/dist/index.js.map +1 -1
  31. package/dist/ingestion/index.d.ts +6 -0
  32. package/dist/ingestion/index.js +10 -0
  33. package/dist/ingestion-BQo4cbGw.js +200 -0
  34. package/dist/ingestion-BQo4cbGw.js.map +1 -0
  35. package/dist/manifest/index.d.ts +2 -0
  36. package/dist/manifest/index.js +4 -0
  37. package/dist/manifest-ByE6YPG0.js +221 -0
  38. package/dist/manifest-ByE6YPG0.js.map +1 -0
  39. package/dist/metrics-CVAk1ALR.d.ts +53 -0
  40. package/dist/metrics-CWPy2Aqc.js +96 -0
  41. package/dist/metrics-CWPy2Aqc.js.map +1 -0
  42. package/dist/metrics.d.ts +4 -0
  43. package/dist/metrics.js +3 -0
  44. package/dist/publisher/index.d.ts +5 -0
  45. package/dist/publisher/index.js +6 -0
  46. package/dist/publisher-BYT3RLVO.js +131 -0
  47. package/dist/publisher-BYT3RLVO.js.map +1 -0
  48. package/dist/runtime-config/index.d.ts +2 -0
  49. package/dist/runtime-config/index.js +4 -0
  50. package/dist/runtime-config-BpB-d_bF.js +51 -0
  51. package/dist/runtime-config-BpB-d_bF.js.map +1 -0
  52. package/dist/server-CXTQ7odl.js +183 -0
  53. package/dist/server-CXTQ7odl.js.map +1 -0
  54. package/dist/service-DuFiQwid.js +191 -0
  55. package/dist/service-DuFiQwid.js.map +1 -0
  56. package/dist/service-IE5qaf9x.d.ts +32 -0
  57. package/dist/state-store/index.d.ts +3 -0
  58. package/dist/state-store/index.js +5 -0
  59. package/dist/state-store-yl6LiTQ0.js +198 -0
  60. package/dist/state-store-yl6LiTQ0.js.map +1 -0
  61. package/dist/status/index.d.ts +5 -0
  62. package/dist/status/index.js +6 -0
  63. package/dist/status-B-oZBc33.js +84 -0
  64. package/dist/status-B-oZBc33.js.map +1 -0
  65. package/dist/types-B35J5YPu.d.ts +174 -0
  66. package/dist/webhook/index.d.ts +6 -0
  67. package/dist/webhook/index.js +8 -0
  68. package/dist/webhook-DI-Afxkg.js +13 -0
  69. package/dist/webhook-DI-Afxkg.js.map +1 -0
  70. package/package.json +66 -8
  71. package/src/auth/index.ts +1 -0
  72. package/src/auth/token-cache.ts +54 -0
  73. package/src/connector-app/check.ts +76 -0
  74. package/src/connector-app/index.ts +2 -0
  75. package/src/connector-app/start.ts +42 -0
  76. package/src/core/builder.ts +81 -9
  77. package/src/core/index.ts +22 -0
  78. package/src/core/types.ts +204 -76
  79. package/src/errors/index.ts +1 -0
  80. package/src/index.ts +13 -28
  81. package/src/ingestion/engine.ts +409 -174
  82. package/src/ingestion/index.ts +1 -0
  83. package/src/manifest/config.ts +232 -0
  84. package/src/manifest/index.ts +14 -0
  85. package/src/manifest/manifest.ts +31 -0
  86. package/src/manifest/number-schema.ts +17 -0
  87. package/src/manifest/schema.ts +160 -0
  88. package/src/metrics.ts +128 -0
  89. package/src/publisher/console.ts +18 -0
  90. package/src/publisher/index.ts +7 -0
  91. package/src/publisher/instrumented.ts +68 -0
  92. package/src/publisher/service.ts +30 -13
  93. package/src/publisher/wings.ts +159 -72
  94. package/src/runtime/context.ts +15 -0
  95. package/src/runtime-config/constants.ts +27 -0
  96. package/src/runtime-config/error.ts +13 -0
  97. package/src/runtime-config/index.ts +3 -0
  98. package/src/runtime-config/layer.ts +56 -0
  99. package/src/state-store/index.ts +3 -0
  100. package/src/state-store/keys.ts +22 -0
  101. package/src/state-store/layer-key-value-store.ts +132 -0
  102. package/src/state-store/layer-memory.ts +11 -0
  103. package/src/state-store/layer-sql.ts +50 -0
  104. package/src/state-store/schema.ts +57 -0
  105. package/src/state-store/service.ts +51 -0
  106. package/src/state-store/state.ts +17 -0
  107. package/src/status/index.ts +2 -0
  108. package/src/status/route.ts +77 -0
  109. package/src/status/types.ts +33 -0
  110. package/src/telemetry.ts +288 -0
  111. package/src/webhook/index.ts +8 -0
  112. package/src/webhook/server.ts +209 -39
  113. package/src/webhook/types.ts +6 -17
  114. package/src/ingestion/state-store.ts +0 -36
  115. package/src/streams/pull-stream.ts +0 -54
  116. package/src/streams/webhook-queue.ts +0 -14
  117. /package/src/{core/errors.ts → errors/connector-error.ts} +0 -0
@@ -0,0 +1,57 @@
1
+ import { Schema } from "effect";
2
+
3
+ const CursorValueSchema = Schema.Union([Schema.String, Schema.Number]);
4
+
5
+ /** Durable progress for a finite historical backfill. */
6
+ export const BackfillStateSchema = Schema.Struct({
7
+ cutoff: CursorValueSchema,
8
+ pageCursor: Schema.optional(CursorValueSchema),
9
+ completed: Schema.Boolean,
10
+ lastSuccessAt: Schema.optional(Schema.String),
11
+ });
12
+
13
+ /** Durable cursor for an ongoing incremental changes stream. */
14
+ export const ChangesStateSchema = Schema.Struct({
15
+ cursor: CursorValueSchema,
16
+ lastSuccessAt: Schema.optional(Schema.String),
17
+ });
18
+
19
+ export const StateSourceSchema = Schema.Literals(["backfill", "changes"]);
20
+
21
+ export const StateOperationSchema = Schema.Literals(["fetch", "publish", "checkpoint"]);
22
+
23
+ export const StateErrorCodeSchema = Schema.Literals([
24
+ "fetch_failed",
25
+ "publish_failed",
26
+ "checkpoint_failed",
27
+ ]);
28
+
29
+ /** SDK-owned error context safe to persist and expose through status. */
30
+ export const ResourceErrorContextSchema = Schema.Struct({
31
+ source: StateSourceSchema,
32
+ operation: StateOperationSchema,
33
+ code: StateErrorCodeSchema,
34
+ message: Schema.String,
35
+ });
36
+
37
+ /** Stable public error summary persisted for status reporting; never a raw cause. */
38
+ export const ResourceErrorSchema = Schema.Struct({
39
+ ...ResourceErrorContextSchema.fields,
40
+ at: Schema.String,
41
+ });
42
+
43
+ /** Complete persisted state assembled from independently stored components. */
44
+ export const ResourceStateSchema = Schema.Struct({
45
+ backfill: Schema.optional(BackfillStateSchema),
46
+ changes: Schema.optional(ChangesStateSchema),
47
+ lastError: Schema.optional(ResourceErrorSchema),
48
+ });
49
+
50
+ export type PersistedBackfillState = Schema.Schema.Type<typeof BackfillStateSchema>;
51
+ export type PersistedChangesState = Schema.Schema.Type<typeof ChangesStateSchema>;
52
+ export type StateSource = Schema.Schema.Type<typeof StateSourceSchema>;
53
+ export type StateOperation = Schema.Schema.Type<typeof StateOperationSchema>;
54
+ export type StateErrorCode = Schema.Schema.Type<typeof StateErrorCodeSchema>;
55
+ export type ResourceErrorContext = Schema.Schema.Type<typeof ResourceErrorContextSchema>;
56
+ export type PersistedResourceError = Schema.Schema.Type<typeof ResourceErrorSchema>;
57
+ export type PersistedResourceState = Schema.Schema.Type<typeof ResourceStateSchema>;
@@ -0,0 +1,51 @@
1
+ import { Context, Effect } from "effect";
2
+
3
+ import type {
4
+ PersistedBackfillState,
5
+ PersistedChangesState,
6
+ PersistedResourceState,
7
+ StateOperation,
8
+ StateSource,
9
+ } from "./schema";
10
+
11
+ import { ConnectorError } from "../errors";
12
+
13
+ /** Connector-facing durable state operations, independent of the storage backend. */
14
+ export interface StateStoreService {
15
+ readonly getResourceState: (
16
+ resource: string,
17
+ ) => Effect.Effect<PersistedResourceState | undefined, ConnectorError>;
18
+ readonly setBackfillState: (
19
+ resource: string,
20
+ state: PersistedBackfillState,
21
+ ) => Effect.Effect<void, ConnectorError>;
22
+ readonly setChangesState: (
23
+ resource: string,
24
+ state: PersistedChangesState,
25
+ ) => Effect.Effect<void, ConnectorError>;
26
+ readonly setResourceError: (
27
+ resource: string,
28
+ source: StateSource,
29
+ operation: StateOperation,
30
+ ) => Effect.Effect<void, ConnectorError>;
31
+ readonly clearResourceError: (
32
+ resource: string,
33
+ source: StateSource,
34
+ ) => Effect.Effect<void, ConnectorError>;
35
+ }
36
+
37
+ /**
38
+ * Effect service used by ingestion and status logic for connector checkpoints.
39
+ * Production provides `layerSql`; sandboxes and tests may provide `layerMemory`.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * const program = Effect.gen(function* () {
44
+ * const state = yield* StateStore;
45
+ * yield* state.setChangesState("orders", { cursor: "next-page" });
46
+ * });
47
+ * ```
48
+ */
49
+ export class StateStore extends Context.Service<StateStore, StateStoreService>()(
50
+ "@useairfoil/connector-kit/StateStore",
51
+ ) {}
@@ -0,0 +1,17 @@
1
+ import type { Cursor, ResourceDefinition, ResourceState, SyncState } from "../core/types";
2
+
3
+ /** Normalizes a cursor value to its persisted/wire representation (never a Date). */
4
+ export const normalizeCursor = (value: Cursor.Value): string | number =>
5
+ value instanceof Date ? value.toISOString() : value;
6
+
7
+ /** Derives connector-facing sync state from durable state and resource capabilities. */
8
+ export const deriveSyncState = (
9
+ resource: ResourceDefinition,
10
+ state: ResourceState | undefined,
11
+ ): SyncState => {
12
+ if (state?.lastError) return "error";
13
+ if (!resource.backfill && !resource.changes) return "live";
14
+ if (!state) return "pending";
15
+ if (resource.backfill && (!state.backfill || !state.backfill.completed)) return "backfilling";
16
+ return "live";
17
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./route";
2
+ export * from "./types";
@@ -0,0 +1,77 @@
1
+ import { Cause, Effect } from "effect";
2
+ import { HttpRouter, HttpServerResponse } from "effect/unstable/http";
3
+
4
+ import type { ConnectorDefinition } from "../core/types";
5
+ import type { ResourceStatus, StatusUnavailableResponse } from "./types";
6
+
7
+ import { StateStore } from "../state-store";
8
+ import { deriveSyncState, normalizeCursor } from "../state-store/state";
9
+
10
+ export const statusRoute = (
11
+ connector: ConnectorDefinition,
12
+ path: HttpRouter.PathInput = "/status",
13
+ ) =>
14
+ HttpRouter.add(
15
+ "GET",
16
+ path,
17
+ Effect.gen(function* () {
18
+ const store = yield* StateStore;
19
+
20
+ const resources = yield* Effect.forEach(
21
+ connector.resources,
22
+ (resource) =>
23
+ store.getResourceState(resource.name).pipe(
24
+ Effect.map(
25
+ (state): ResourceStatus => ({
26
+ name: resource.name,
27
+ state: deriveSyncState(resource, state),
28
+ ...(state?.backfill
29
+ ? {
30
+ backfill: {
31
+ completed: state.backfill.completed,
32
+ cutoff: normalizeCursor(state.backfill.cutoff),
33
+ ...(state.backfill.pageCursor !== undefined
34
+ ? { pageCursor: normalizeCursor(state.backfill.pageCursor) }
35
+ : {}),
36
+ ...(state.backfill.lastSuccessAt !== undefined
37
+ ? { lastSuccessAt: state.backfill.lastSuccessAt }
38
+ : {}),
39
+ },
40
+ }
41
+ : {}),
42
+ ...(state?.changes
43
+ ? {
44
+ changes: {
45
+ cursor: normalizeCursor(state.changes.cursor),
46
+ ...(state.changes.lastSuccessAt !== undefined
47
+ ? { lastSuccessAt: state.changes.lastSuccessAt }
48
+ : {}),
49
+ },
50
+ }
51
+ : {}),
52
+ ...(state?.lastError ? { lastError: state.lastError } : {}),
53
+ }),
54
+ ),
55
+ ),
56
+ { concurrency: "unbounded" },
57
+ );
58
+
59
+ return HttpServerResponse.jsonUnsafe({ connector: connector.name, resources });
60
+ }).pipe(
61
+ Effect.catchCause((cause) =>
62
+ Effect.logWarning(`Status route failed: ${Cause.pretty(cause)}`).pipe(
63
+ Effect.andThen(
64
+ Effect.succeed(
65
+ HttpServerResponse.jsonUnsafe(
66
+ {
67
+ ok: false,
68
+ error: "Failed to compute status",
69
+ } satisfies StatusUnavailableResponse,
70
+ { status: 500 },
71
+ ),
72
+ ),
73
+ ),
74
+ ),
75
+ ),
76
+ ),
77
+ );
@@ -0,0 +1,33 @@
1
+ import { Schema } from "effect";
2
+
3
+ import {
4
+ ResourceErrorSchema as StateResourceErrorSchema,
5
+ ResourceStateSchema,
6
+ } from "../state-store/schema";
7
+
8
+ export const SyncStateSchema = Schema.Literals(["pending", "backfilling", "live", "error"]);
9
+ export type SyncState = Schema.Schema.Type<typeof SyncStateSchema>;
10
+
11
+ /** Safe source failure returned by the status endpoint. */
12
+ export const ResourceErrorSchema = StateResourceErrorSchema;
13
+ export type ResourceError = Schema.Schema.Type<typeof ResourceErrorSchema>;
14
+
15
+ export const ResourceStatusSchema = Schema.Struct({
16
+ name: Schema.String,
17
+ state: SyncStateSchema,
18
+ ...ResourceStateSchema.fields,
19
+ });
20
+ export type ResourceStatus = Schema.Schema.Type<typeof ResourceStatusSchema>;
21
+
22
+ export const StatusResponseSchema = Schema.Struct({
23
+ connector: Schema.String,
24
+ resources: Schema.Array(ResourceStatusSchema),
25
+ });
26
+ export type StatusResponse = Schema.Schema.Type<typeof StatusResponseSchema>;
27
+
28
+ /** Returned when durable resource status cannot be read. */
29
+ export const StatusUnavailableResponseSchema = Schema.Struct({
30
+ ok: Schema.Literal(false),
31
+ error: Schema.String,
32
+ });
33
+ export type StatusUnavailableResponse = Schema.Schema.Type<typeof StatusUnavailableResponseSchema>;
@@ -0,0 +1,288 @@
1
+ import { Cause, Config, Effect, Layer, Metric, Option, type Duration } from "effect";
2
+ import { Headers } from "effect/unstable/http";
3
+ import * as Observability from "effect/unstable/observability";
4
+
5
+ import { PlatformRuntimeKey } from "./runtime-config/constants";
6
+
7
+ /** Canonical span names emitted by connector-kit runtime instrumentation. */
8
+ export const SpanName = {
9
+ batchProcess: "connector.batch.process",
10
+ webhookDecode: "connector.webhook.decode",
11
+ webhookHandle: "connector.webhook.handle",
12
+ publish: "connector.publish",
13
+ stateSet: "connector.state.set",
14
+ apiFetch: "connector.api.fetch",
15
+ } as const;
16
+
17
+ /** Canonical span attribute names. Keep these stable for collector queries. */
18
+ export const Attr = {
19
+ connectorName: "airfoil.connector.name",
20
+ connectorResourcesCount: "airfoil.connector.resources.count",
21
+ resourceName: "airfoil.resource.name",
22
+ resourceSource: "airfoil.resource.source",
23
+ batchMutations: "airfoil.batch.mutations",
24
+ webhookPath: "airfoil.webhook.path",
25
+ publisherSuccess: "airfoil.publisher.success",
26
+ stateKey: "airfoil.state.key",
27
+ apiPath: "airfoil.api.path",
28
+ errorPhase: "airfoil.error.phase",
29
+ errorType: "airfoil.error.type",
30
+ errorMessage: "airfoil.error.message",
31
+ errorDetails: "airfoil.error.details",
32
+ } as const;
33
+
34
+ /** Span event names for high-cardinality reproduction details. */
35
+ export const EventName = {
36
+ batchCheckpoint: "airfoil.batch.checkpoint",
37
+ } as const;
38
+
39
+ /** Event-only attributes. These are intentionally not span attributes. */
40
+ export const EventAttr = {
41
+ batchCursor: "airfoil.batch.cursor",
42
+ } as const;
43
+
44
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
45
+ typeof value === "object" && value !== null;
46
+
47
+ const maxErrorMessageLength = 500;
48
+
49
+ const OtlpEnvConfig = Config.all({
50
+ enabled: Config.boolean(PlatformRuntimeKey.otelEnabled).pipe(Config.withDefault(false)),
51
+ baseUrl: Config.option(Config.string(PlatformRuntimeKey.otelExporterOtlpEndpoint)),
52
+ rawHeaders: Config.option(Config.string(PlatformRuntimeKey.otelExporterOtlpHeaders)),
53
+ });
54
+
55
+ const defaultRedactedHeaders: ReadonlyArray<string | RegExp> = [
56
+ "authorization",
57
+ "cookie",
58
+ "set-cookie",
59
+ "x-api-key",
60
+ /api[-_]?key/i,
61
+ /secret/i,
62
+ /signature/i,
63
+ /token/i,
64
+ ];
65
+
66
+ const parseOtelHeaders = (value: string): Record<string, string> =>
67
+ Object.fromEntries(
68
+ value
69
+ .split(",")
70
+ .map((entry) => entry.trim())
71
+ .filter(Boolean)
72
+ .flatMap((entry) => {
73
+ const separator = entry.indexOf("=");
74
+ if (separator < 1) return [];
75
+ return [[entry.slice(0, separator).trim(), entry.slice(separator + 1).trim()]];
76
+ }),
77
+ );
78
+
79
+ const appendPath = (baseUrl: string, path: string) =>
80
+ `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
81
+
82
+ /**
83
+ * Best-effort stable error classifier for span metadata.
84
+ *
85
+ * Effect/Data tagged errors expose `_tag`; that is the most useful value for
86
+ * connector bugs (`ConnectorError`, `InvalidWebhookPayloadError`, etc.). For
87
+ * non-tagged errors we fall back to the JS `Error.name`, then constructor name,
88
+ * then the primitive type.
89
+ */
90
+ const errorType = (error: unknown) => {
91
+ if (isRecord(error) && typeof error._tag === "string") {
92
+ return String(error._tag);
93
+ }
94
+ if (error instanceof Error) {
95
+ return error.name;
96
+ }
97
+ if (isRecord(error) && typeof error.constructor?.name === "string") {
98
+ return error.constructor.name;
99
+ }
100
+ return typeof error;
101
+ };
102
+
103
+ /**
104
+ * Human-readable error summary for span metadata.
105
+ *
106
+ * Tagged errors in this package carry a `message` field, so this preserves the
107
+ * important diagnostic text. Unknown object failures are intentionally reduced
108
+ * to their type; full structured error payloads belong in logs, not span attrs.
109
+ */
110
+ const errorMessage = (error: unknown) => {
111
+ const message = (() => {
112
+ if (isRecord(error) && typeof error.message === "string") {
113
+ return error.message;
114
+ }
115
+ if (typeof error === "string") {
116
+ return error;
117
+ }
118
+ return errorType(error);
119
+ })();
120
+
121
+ if (message.length > maxErrorMessageLength) {
122
+ return `${message.slice(0, maxErrorMessageLength)}...`;
123
+ }
124
+
125
+ return message;
126
+ };
127
+
128
+ const errorDetails = (error: unknown): string | undefined => {
129
+ if (!isRecord(error) || error.cause === undefined) return undefined;
130
+ return Cause.pretty(Cause.die(error.cause));
131
+ };
132
+
133
+ /** Annotates the active span with phase-aware error metadata before re-failing. */
134
+ export const annotateError = (phase: string, error: unknown) => {
135
+ const details = errorDetails(error);
136
+ return Effect.annotateCurrentSpan({
137
+ [Attr.errorPhase]: phase,
138
+ [Attr.errorType]: errorType(error),
139
+ [Attr.errorMessage]: errorMessage(error),
140
+ ...(details !== undefined ? { [Attr.errorDetails]: details } : {}),
141
+ });
142
+ };
143
+
144
+ /** Adds an event to the active span, or no-ops when no span is active. */
145
+ export const addCurrentSpanEvent = (name: string, attributes?: Record<string, unknown>) =>
146
+ Effect.currentSpan.pipe(
147
+ Effect.flatMap((span) =>
148
+ Effect.clockWith((clock) =>
149
+ Effect.sync(() => span.event(name, clock.currentTimeNanosUnsafe(), attributes)),
150
+ ),
151
+ ),
152
+ Effect.ignore,
153
+ );
154
+
155
+ /** Transport configuration for OTLP trace export. All fields are direct values. */
156
+ export type OtlpTracingConfig = {
157
+ readonly enabled?: boolean;
158
+ readonly endpoint?: string;
159
+ readonly headers?: Record<string, string>;
160
+ };
161
+
162
+ /** Runtime options — not config-wrappable since RegExp cannot live in Config. */
163
+ export type OtlpTracingOptions = {
164
+ /** Additional connector-specific header names or regexes to redact from HTTP logs/traces. */
165
+ readonly redactedHeaders?: ReadonlyArray<string | RegExp>;
166
+ };
167
+
168
+ export type OtlpMetricsOptions = {
169
+ readonly exportInterval?: Duration.Input;
170
+ };
171
+
172
+ const buildOtlpTracingLayer = (config: OtlpTracingConfig, options: OtlpTracingOptions) =>
173
+ Effect.gen(function* () {
174
+ const allRedacted = [...defaultRedactedHeaders, ...(options.redactedHeaders ?? [])];
175
+ const RedactionLayer = Layer.succeed(Headers.CurrentRedactedNames)(allRedacted);
176
+
177
+ if (!config.enabled) {
178
+ return RedactionLayer;
179
+ }
180
+
181
+ if (!config.endpoint) {
182
+ return yield* Effect.fail(new Error("enabled=true requires endpoint"));
183
+ }
184
+
185
+ yield* Effect.logInfo("telemetry enabled").pipe(
186
+ Effect.annotateLogs({
187
+ endpoint: config.endpoint,
188
+ headers: config.headers ? Object.keys(config.headers) : [],
189
+ }),
190
+ );
191
+
192
+ return Layer.mergeAll(
193
+ RedactionLayer,
194
+ Observability.OtlpTracer.layer({
195
+ url: appendPath(config.endpoint, "/v1/traces"),
196
+ headers: config.headers,
197
+ }).pipe(Layer.provide(Observability.OtlpSerialization.layerJson)),
198
+ );
199
+ });
200
+
201
+ /**
202
+ * Trace-only OTLP export with sensitive HTTP header redaction.
203
+ * Takes direct config values — no `ConfigProvider` required.
204
+ */
205
+ export const layer = (config: OtlpTracingConfig = {}, options: OtlpTracingOptions = {}) =>
206
+ Layer.unwrap(buildOtlpTracingLayer(config, options));
207
+
208
+ /**
209
+ * Trace-only OTLP export with sensitive HTTP header redaction.
210
+ * Takes Effect Config-wrapped values — reads from `ConfigProvider`.
211
+ *
212
+ * @example
213
+ * Telemetry.layerConfig({
214
+ * enabled: Config.boolean("MY_OTEL_ENABLED"),
215
+ * endpoint: Config.string("MY_COLLECTOR_URL"),
216
+ * })
217
+ */
218
+ export const layerConfig = (
219
+ config: Config.Wrap<OtlpTracingConfig>,
220
+ options: OtlpTracingOptions = {},
221
+ ) =>
222
+ Layer.unwrap(
223
+ Effect.gen(function* () {
224
+ const resolved = yield* Config.unwrap(config);
225
+ return yield* buildOtlpTracingLayer(resolved, options);
226
+ }),
227
+ );
228
+
229
+ /**
230
+ * Trace-only OTLP export with sensitive HTTP header redaction.
231
+ * Zero-config shortcut that reads the standard `OTEL_*` env var names.
232
+ *
233
+ * Reads `OTEL_ENABLED`, `OTEL_EXPORTER_OTLP_ENDPOINT`, and
234
+ * `OTEL_EXPORTER_OTLP_HEADERS`. Effect's OTLP resource handling reads
235
+ * `OTEL_SERVICE_NAME`, `OTEL_SERVICE_VERSION`, and `OTEL_RESOURCE_ATTRIBUTES`.
236
+ */
237
+ export const layerOtlpTracing = (options: OtlpTracingOptions = {}) =>
238
+ Layer.unwrap(
239
+ Effect.gen(function* () {
240
+ const raw = yield* OtlpEnvConfig;
241
+ return yield* buildOtlpTracingLayer(
242
+ {
243
+ enabled: raw.enabled,
244
+ endpoint: Option.getOrUndefined(raw.baseUrl),
245
+ headers: Option.isSome(raw.rawHeaders)
246
+ ? parseOtelHeaders(raw.rawHeaders.value)
247
+ : undefined,
248
+ },
249
+ options,
250
+ );
251
+ }),
252
+ );
253
+
254
+ export const layerOtlpMetrics = (options: OtlpMetricsOptions = {}) =>
255
+ Layer.unwrap(
256
+ Effect.gen(function* () {
257
+ const raw = yield* OtlpEnvConfig;
258
+ if (!raw.enabled) return Layer.empty;
259
+ const endpoint = Option.getOrUndefined(raw.baseUrl);
260
+ if (!endpoint)
261
+ return yield* Effect.fail(
262
+ new Error("OTEL_ENABLED=true requires OTEL_EXPORTER_OTLP_ENDPOINT"),
263
+ );
264
+ return Observability.OtlpMetrics.layer({
265
+ url: appendPath(endpoint, "/v1/metrics"),
266
+ headers: Option.isSome(raw.rawHeaders) ? parseOtelHeaders(raw.rawHeaders.value) : undefined,
267
+ exportInterval: options.exportInterval,
268
+ temporality: "cumulative",
269
+ }).pipe(Layer.provide(Observability.OtlpSerialization.layerJson));
270
+ }),
271
+ );
272
+
273
+ export const layerOtlp = (options: OtlpTracingOptions & OtlpMetricsOptions = {}) =>
274
+ Layer.mergeAll(layerOtlpTracing(options), layerOtlpMetrics(options));
275
+
276
+ export const layerMetricsConsoleDump = (interval: Duration.Input = "30 seconds") =>
277
+ Layer.effectDiscard(
278
+ Effect.gen(function* () {
279
+ yield* Effect.forkScoped(
280
+ Effect.forever(
281
+ Metric.dump.pipe(
282
+ Effect.flatMap((snapshot) => Effect.logInfo(snapshot)),
283
+ Effect.andThen(Effect.sleep(interval)),
284
+ ),
285
+ ),
286
+ );
287
+ }),
288
+ );
@@ -0,0 +1,8 @@
1
+ import type { ResourceDefinition, WebhookRoute, WebhookRouteInput } from "../core/types";
2
+
3
+ export { router } from "./server";
4
+ export type { WebhookAckMode, WebhookRoute, WebhookRouteContext, WebhookRouteInput } from "./types";
5
+
6
+ export const route = <const Resources extends ReadonlyArray<ResourceDefinition>, Payload>(
7
+ definition: WebhookRouteInput<Resources, Payload>,
8
+ ): WebhookRoute<Resources, Payload> => definition;