@cleocode/lafs-protocol 0.5.0 → 1.1.0

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 (48) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +7 -3
  3. package/dist/examples/discovery-server.d.ts +8 -0
  4. package/dist/examples/discovery-server.js +216 -0
  5. package/dist/examples/mcp-lafs-client.d.ts +10 -0
  6. package/dist/examples/mcp-lafs-client.js +427 -0
  7. package/dist/examples/mcp-lafs-server.d.ts +10 -0
  8. package/dist/examples/mcp-lafs-server.js +358 -0
  9. package/dist/schemas/v1/envelope.schema.json +0 -0
  10. package/dist/schemas/v1/error-registry.json +0 -0
  11. package/dist/src/a2a/bridge.d.ts +129 -0
  12. package/dist/src/a2a/bridge.js +173 -0
  13. package/dist/src/a2a/index.d.ts +36 -0
  14. package/dist/src/a2a/index.js +36 -0
  15. package/dist/src/budgetEnforcement.d.ts +84 -0
  16. package/dist/src/budgetEnforcement.js +328 -0
  17. package/dist/src/circuit-breaker/index.d.ts +121 -0
  18. package/dist/src/circuit-breaker/index.js +249 -0
  19. package/dist/src/cli.d.ts +0 -0
  20. package/dist/src/cli.js +0 -0
  21. package/dist/src/conformance.d.ts +0 -0
  22. package/dist/src/conformance.js +0 -0
  23. package/dist/src/discovery.d.ts +127 -0
  24. package/dist/src/discovery.js +304 -0
  25. package/dist/src/errorRegistry.d.ts +0 -0
  26. package/dist/src/errorRegistry.js +0 -0
  27. package/dist/src/flagSemantics.d.ts +0 -0
  28. package/dist/src/flagSemantics.js +0 -0
  29. package/dist/src/health/index.d.ts +105 -0
  30. package/dist/src/health/index.js +211 -0
  31. package/dist/src/index.d.ts +8 -0
  32. package/dist/src/index.js +10 -0
  33. package/dist/src/mcpAdapter.d.ts +28 -0
  34. package/dist/src/mcpAdapter.js +281 -0
  35. package/dist/src/shutdown/index.d.ts +69 -0
  36. package/dist/src/shutdown/index.js +160 -0
  37. package/dist/src/tokenEstimator.d.ts +87 -0
  38. package/dist/src/tokenEstimator.js +238 -0
  39. package/dist/src/types.d.ts +25 -0
  40. package/dist/src/types.js +0 -0
  41. package/dist/src/validateEnvelope.d.ts +0 -0
  42. package/dist/src/validateEnvelope.js +0 -0
  43. package/lafs.md +167 -0
  44. package/package.json +10 -4
  45. package/schemas/v1/context-ledger.schema.json +0 -0
  46. package/schemas/v1/discovery.schema.json +132 -0
  47. package/schemas/v1/envelope.schema.json +0 -0
  48. package/schemas/v1/error-registry.json +0 -0
@@ -0,0 +1,249 @@
1
+ /**
2
+ * LAFS Circuit Breaker Module
3
+ *
4
+ * Provides circuit breaker pattern for resilient service calls
5
+ */
6
+ export class CircuitBreakerError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = 'CircuitBreakerError';
10
+ }
11
+ }
12
+ /**
13
+ * Circuit breaker for protecting against cascading failures
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { CircuitBreaker } from '@cleocode/lafs-protocol/circuit-breaker';
18
+ *
19
+ * const breaker = new CircuitBreaker({
20
+ * name: 'external-api',
21
+ * failureThreshold: 5,
22
+ * resetTimeout: 30000
23
+ * });
24
+ *
25
+ * try {
26
+ * const result = await breaker.execute(async () => {
27
+ * return await externalApi.call();
28
+ * });
29
+ * } catch (error) {
30
+ * if (error instanceof CircuitBreakerError) {
31
+ * console.log('Circuit breaker is open');
32
+ * }
33
+ * }
34
+ * ```
35
+ */
36
+ export class CircuitBreaker {
37
+ config;
38
+ state = 'CLOSED';
39
+ failures = 0;
40
+ successes = 0;
41
+ lastFailureTime;
42
+ consecutiveSuccesses = 0;
43
+ totalCalls = 0;
44
+ halfOpenCalls = 0;
45
+ resetTimer;
46
+ constructor(config) {
47
+ this.config = config;
48
+ this.config = {
49
+ failureThreshold: 5,
50
+ resetTimeout: 30000,
51
+ halfOpenMaxCalls: 3,
52
+ successThreshold: 2,
53
+ ...config
54
+ };
55
+ }
56
+ /**
57
+ * Execute a function with circuit breaker protection
58
+ */
59
+ async execute(fn) {
60
+ this.totalCalls++;
61
+ if (this.state === 'OPEN') {
62
+ if (this.shouldAttemptReset()) {
63
+ this.transitionTo('HALF_OPEN');
64
+ }
65
+ else {
66
+ throw new CircuitBreakerError(`Circuit breaker '${this.config.name}' is OPEN`);
67
+ }
68
+ }
69
+ if (this.state === 'HALF_OPEN') {
70
+ if (this.halfOpenCalls >= (this.config.halfOpenMaxCalls || 3)) {
71
+ throw new CircuitBreakerError(`Circuit breaker '${this.config.name}' is HALF_OPEN (max calls reached)`);
72
+ }
73
+ this.halfOpenCalls++;
74
+ }
75
+ try {
76
+ const result = await fn();
77
+ this.onSuccess();
78
+ return result;
79
+ }
80
+ catch (error) {
81
+ this.onFailure();
82
+ throw error;
83
+ }
84
+ }
85
+ /**
86
+ * Get current circuit breaker state
87
+ */
88
+ getState() {
89
+ return this.state;
90
+ }
91
+ /**
92
+ * Get circuit breaker metrics
93
+ */
94
+ getMetrics() {
95
+ return {
96
+ state: this.state,
97
+ failures: this.failures,
98
+ successes: this.successes,
99
+ lastFailureTime: this.lastFailureTime,
100
+ consecutiveSuccesses: this.consecutiveSuccesses,
101
+ totalCalls: this.totalCalls
102
+ };
103
+ }
104
+ /**
105
+ * Manually open the circuit breaker
106
+ */
107
+ forceOpen() {
108
+ this.transitionTo('OPEN');
109
+ }
110
+ /**
111
+ * Manually close the circuit breaker
112
+ */
113
+ forceClose() {
114
+ this.transitionTo('CLOSED');
115
+ this.reset();
116
+ }
117
+ onSuccess() {
118
+ this.successes++;
119
+ this.consecutiveSuccesses++;
120
+ if (this.state === 'HALF_OPEN') {
121
+ if (this.consecutiveSuccesses >= (this.config.successThreshold || 2)) {
122
+ this.transitionTo('CLOSED');
123
+ this.reset();
124
+ }
125
+ }
126
+ }
127
+ onFailure() {
128
+ this.failures++;
129
+ this.consecutiveSuccesses = 0;
130
+ this.lastFailureTime = new Date();
131
+ if (this.state === 'HALF_OPEN') {
132
+ this.transitionTo('OPEN');
133
+ this.scheduleReset();
134
+ }
135
+ else if (this.state === 'CLOSED') {
136
+ if (this.failures >= (this.config.failureThreshold || 5)) {
137
+ this.transitionTo('OPEN');
138
+ this.scheduleReset();
139
+ }
140
+ }
141
+ }
142
+ transitionTo(newState) {
143
+ console.log(`Circuit breaker '${this.config.name}': ${this.state} -> ${newState}`);
144
+ this.state = newState;
145
+ if (newState === 'HALF_OPEN') {
146
+ this.halfOpenCalls = 0;
147
+ }
148
+ }
149
+ shouldAttemptReset() {
150
+ if (!this.lastFailureTime)
151
+ return true;
152
+ const elapsed = Date.now() - this.lastFailureTime.getTime();
153
+ return elapsed >= (this.config.resetTimeout || 30000);
154
+ }
155
+ scheduleReset() {
156
+ if (this.resetTimer) {
157
+ clearTimeout(this.resetTimer);
158
+ }
159
+ this.resetTimer = setTimeout(() => {
160
+ if (this.state === 'OPEN') {
161
+ this.transitionTo('HALF_OPEN');
162
+ }
163
+ }, this.config.resetTimeout || 30000);
164
+ }
165
+ reset() {
166
+ this.failures = 0;
167
+ this.consecutiveSuccesses = 0;
168
+ this.halfOpenCalls = 0;
169
+ if (this.resetTimer) {
170
+ clearTimeout(this.resetTimer);
171
+ this.resetTimer = undefined;
172
+ }
173
+ }
174
+ }
175
+ /**
176
+ * Circuit breaker registry for managing multiple breakers
177
+ *
178
+ * @example
179
+ * ```typescript
180
+ * const registry = new CircuitBreakerRegistry();
181
+ *
182
+ * registry.add('payment-api', {
183
+ * failureThreshold: 3,
184
+ * resetTimeout: 60000
185
+ * });
186
+ *
187
+ * const paymentBreaker = registry.get('payment-api');
188
+ * ```
189
+ */
190
+ export class CircuitBreakerRegistry {
191
+ breakers = new Map();
192
+ add(name, config) {
193
+ const breaker = new CircuitBreaker({ ...config, name });
194
+ this.breakers.set(name, breaker);
195
+ return breaker;
196
+ }
197
+ get(name) {
198
+ return this.breakers.get(name);
199
+ }
200
+ getOrCreate(name, config) {
201
+ let breaker = this.breakers.get(name);
202
+ if (!breaker) {
203
+ breaker = this.add(name, config);
204
+ }
205
+ return breaker;
206
+ }
207
+ getAllMetrics() {
208
+ const metrics = {};
209
+ this.breakers.forEach((breaker, name) => {
210
+ metrics[name] = breaker.getMetrics();
211
+ });
212
+ return metrics;
213
+ }
214
+ resetAll() {
215
+ this.breakers.forEach(breaker => breaker.forceClose());
216
+ }
217
+ }
218
+ /**
219
+ * Create a circuit breaker middleware for Express
220
+ *
221
+ * @example
222
+ * ```typescript
223
+ * app.use('/external-api', circuitBreakerMiddleware({
224
+ * name: 'external-api',
225
+ * failureThreshold: 5
226
+ * }));
227
+ * ```
228
+ */
229
+ export function circuitBreakerMiddleware(config) {
230
+ const breaker = new CircuitBreaker(config);
231
+ return async (req, res, next) => {
232
+ try {
233
+ await breaker.execute(async () => {
234
+ next();
235
+ });
236
+ }
237
+ catch (error) {
238
+ if (error instanceof CircuitBreakerError) {
239
+ res.status(503).json({
240
+ error: 'Service temporarily unavailable',
241
+ reason: 'Circuit breaker is open'
242
+ });
243
+ }
244
+ else {
245
+ throw error;
246
+ }
247
+ }
248
+ };
249
+ }
package/dist/src/cli.d.ts CHANGED
File without changes
package/dist/src/cli.js CHANGED
File without changes
File without changes
File without changes
@@ -0,0 +1,127 @@
1
+ /**
2
+ * LAFS Agent Discovery - Express/Fastify Middleware
3
+ * Serves discovery document at /.well-known/lafs.json
4
+ */
5
+ import type { RequestHandler } from "express";
6
+ /**
7
+ * Capability definition for service advertisement
8
+ */
9
+ export interface Capability {
10
+ name: string;
11
+ version: string;
12
+ description?: string;
13
+ operations: string[];
14
+ optional?: boolean;
15
+ }
16
+ /**
17
+ * Service configuration for discovery document
18
+ */
19
+ export interface ServiceConfig {
20
+ name: string;
21
+ version: string;
22
+ description?: string;
23
+ }
24
+ /**
25
+ * Endpoint configuration for discovery document
26
+ */
27
+ export interface EndpointConfig {
28
+ envelope: string;
29
+ context?: string;
30
+ discovery: string;
31
+ }
32
+ /**
33
+ * Complete discovery document served at /.well-known/lafs.json
34
+ */
35
+ export interface DiscoveryDocument {
36
+ $schema: string;
37
+ lafs_version: string;
38
+ service: ServiceConfig;
39
+ capabilities: Capability[];
40
+ endpoints: EndpointConfig;
41
+ }
42
+ /**
43
+ * Configuration for the discovery middleware
44
+ */
45
+ export interface DiscoveryConfig {
46
+ /** Service information */
47
+ service: ServiceConfig;
48
+ /** List of capabilities this service provides */
49
+ capabilities: Capability[];
50
+ /** Endpoint URLs - can be relative paths or absolute URLs */
51
+ endpoints: {
52
+ /** URL for envelope submission endpoint */
53
+ envelope: string;
54
+ /** Optional URL for context ledger endpoint */
55
+ context?: string;
56
+ /** URL for this discovery document (usually auto-detected) */
57
+ discovery?: string;
58
+ };
59
+ /** Cache duration in seconds (default: 3600) */
60
+ cacheMaxAge?: number;
61
+ /** LAFS protocol version (default: "1.0.0") */
62
+ lafsVersion?: string;
63
+ /** Schema URL override */
64
+ schemaUrl?: string;
65
+ /** Base URL for constructing absolute URLs */
66
+ baseUrl?: string;
67
+ /** Optional custom headers to include in response */
68
+ headers?: Record<string, string>;
69
+ }
70
+ /**
71
+ * Discovery middleware options
72
+ */
73
+ export interface DiscoveryMiddlewareOptions {
74
+ /** Path to serve discovery document (default: /.well-known/lafs.json) */
75
+ path?: string;
76
+ /** Enable HEAD requests (default: true) */
77
+ enableHead?: boolean;
78
+ /** Enable ETag caching (default: true) */
79
+ enableEtag?: boolean;
80
+ }
81
+ /**
82
+ * Create Express middleware for serving LAFS discovery document
83
+ *
84
+ * @param config - Discovery configuration
85
+ * @param options - Middleware options
86
+ * @returns Express RequestHandler
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * import express from "express";
91
+ * import { discoveryMiddleware } from "./discovery.js";
92
+ *
93
+ * const app = express();
94
+ *
95
+ * app.use(discoveryMiddleware({
96
+ * service: {
97
+ * name: "my-lafs-service",
98
+ * version: "1.0.0",
99
+ * description: "A LAFS-compliant API service"
100
+ * },
101
+ * capabilities: [
102
+ * {
103
+ * name: "envelope-processor",
104
+ * version: "1.0.0",
105
+ * operations: ["process", "validate"],
106
+ * description: "Process LAFS envelopes"
107
+ * }
108
+ * ],
109
+ * endpoints: {
110
+ * envelope: "/api/v1/envelope",
111
+ * context: "/api/v1/context"
112
+ * }
113
+ * }));
114
+ * ```
115
+ */
116
+ export declare function discoveryMiddleware(config: DiscoveryConfig, options?: DiscoveryMiddlewareOptions): RequestHandler;
117
+ /**
118
+ * Fastify plugin for LAFS discovery (for Fastify users)
119
+ *
120
+ * @param fastify - Fastify instance
121
+ * @param options - Plugin options
122
+ */
123
+ export declare function discoveryFastifyPlugin(fastify: unknown, options: {
124
+ config: DiscoveryConfig;
125
+ path?: string;
126
+ }): Promise<void>;
127
+ export default discoveryMiddleware;
@@ -0,0 +1,304 @@
1
+ /**
2
+ * LAFS Agent Discovery - Express/Fastify Middleware
3
+ * Serves discovery document at /.well-known/lafs.json
4
+ */
5
+ import { createRequire } from "node:module";
6
+ import { createHash } from "crypto";
7
+ import { readFileSync } from "fs";
8
+ import { fileURLToPath } from "url";
9
+ import { dirname, join } from "path";
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+ // Handle ESM/CommonJS interop for AJV
13
+ const require = createRequire(import.meta.url);
14
+ const AjvModule = require("ajv");
15
+ const AddFormatsModule = require("ajv-formats");
16
+ const AjvCtor = (typeof AjvModule === "function" ? AjvModule : AjvModule.default);
17
+ const addFormats = (typeof AddFormatsModule === "function" ? AddFormatsModule : AddFormatsModule.default);
18
+ let ajvInstance = null;
19
+ let validateDiscovery = null;
20
+ /**
21
+ * Initialize AJV validator for discovery documents
22
+ */
23
+ function initValidator() {
24
+ if (ajvInstance && validateDiscovery)
25
+ return;
26
+ ajvInstance = new AjvCtor({ strict: true, allErrors: true });
27
+ addFormats(ajvInstance);
28
+ try {
29
+ // Try to load schema from schemas directory
30
+ const schemaPath = join(__dirname, "..", "..", "schemas", "v1", "discovery.schema.json");
31
+ const schema = JSON.parse(readFileSync(schemaPath, "utf-8"));
32
+ validateDiscovery = ajvInstance.compile(schema);
33
+ }
34
+ catch (e) {
35
+ // Fallback to inline schema if file not found
36
+ const fallbackSchema = {
37
+ $schema: "http://json-schema.org/draft-07/schema#",
38
+ type: "object",
39
+ required: ["$schema", "lafs_version", "service", "capabilities", "endpoints"],
40
+ properties: {
41
+ $schema: { type: "string", format: "uri" },
42
+ lafs_version: { type: "string", pattern: "^\\d+\\.\\d+\\.\\d+$" },
43
+ service: {
44
+ type: "object",
45
+ required: ["name", "version"],
46
+ properties: {
47
+ name: { type: "string", minLength: 1 },
48
+ version: { type: "string", pattern: "^\\d+\\.\\d+\\.\\d+$" },
49
+ description: { type: "string" }
50
+ }
51
+ },
52
+ capabilities: {
53
+ type: "array",
54
+ items: {
55
+ type: "object",
56
+ required: ["name", "version", "operations"],
57
+ properties: {
58
+ name: { type: "string", minLength: 1 },
59
+ version: { type: "string", pattern: "^\\d+\\.\\d+\\.\\d+$" },
60
+ description: { type: "string" },
61
+ operations: { type: "array", items: { type: "string" } },
62
+ optional: { type: "boolean" }
63
+ }
64
+ }
65
+ },
66
+ endpoints: {
67
+ type: "object",
68
+ required: ["envelope", "discovery"],
69
+ properties: {
70
+ envelope: { type: "string", minLength: 1 },
71
+ context: { type: "string", minLength: 1 },
72
+ discovery: { type: "string", minLength: 1 }
73
+ }
74
+ }
75
+ }
76
+ };
77
+ validateDiscovery = ajvInstance.compile(fallbackSchema);
78
+ }
79
+ }
80
+ /**
81
+ * Build absolute URL from base and path
82
+ */
83
+ function buildUrl(base, path, req) {
84
+ // If path is already absolute, return it
85
+ if (path.startsWith("http://") || path.startsWith("https://")) {
86
+ return path;
87
+ }
88
+ // If base is provided, use it
89
+ if (base) {
90
+ const separator = base.endsWith("/") || path.startsWith("/") ? "" : "/";
91
+ return `${base}${separator}${path}`;
92
+ }
93
+ // Otherwise try to construct from request
94
+ if (req) {
95
+ const protocol = req.headers["x-forwarded-proto"] || req.protocol || "http";
96
+ const host = req.headers.host || "localhost";
97
+ const separator = path.startsWith("/") ? "" : "/";
98
+ return `${protocol}://${host}${separator}${path}`;
99
+ }
100
+ // Fallback to relative path
101
+ return path.startsWith("/") ? path : `/${path}`;
102
+ }
103
+ /**
104
+ * Generate ETag from document content
105
+ */
106
+ function generateETag(content) {
107
+ return `"${createHash("sha256").update(content).digest("hex").slice(0, 32)}"`;
108
+ }
109
+ /**
110
+ * Build discovery document from configuration
111
+ */
112
+ function buildDiscoveryDocument(config, req) {
113
+ const schemaUrl = config.schemaUrl || "https://lafs.dev/schemas/v1/discovery.schema.json";
114
+ const lafsVersion = config.lafsVersion || "1.0.0";
115
+ return {
116
+ $schema: schemaUrl,
117
+ lafs_version: lafsVersion,
118
+ service: config.service,
119
+ capabilities: config.capabilities,
120
+ endpoints: {
121
+ envelope: buildUrl(config.baseUrl, config.endpoints.envelope, req),
122
+ context: config.endpoints.context
123
+ ? buildUrl(config.baseUrl, config.endpoints.context, req)
124
+ : undefined,
125
+ discovery: config.endpoints.discovery
126
+ ? buildUrl(config.baseUrl, config.endpoints.discovery, req)
127
+ : buildUrl(config.baseUrl, "/.well-known/lafs.json", req)
128
+ }
129
+ };
130
+ }
131
+ /**
132
+ * Validate discovery document against schema
133
+ */
134
+ function validateDocument(doc) {
135
+ initValidator();
136
+ if (!validateDiscovery) {
137
+ throw new Error("Discovery document validator not initialized");
138
+ }
139
+ const valid = validateDiscovery(doc);
140
+ if (!valid) {
141
+ const errors = validateDiscovery.errors;
142
+ const errorMessages = errors?.map((e) => `${e.instancePath || "root"}: ${e.message}`).join("; ");
143
+ throw new Error(`Discovery document validation failed: ${errorMessages}`);
144
+ }
145
+ }
146
+ /**
147
+ * Create Express middleware for serving LAFS discovery document
148
+ *
149
+ * @param config - Discovery configuration
150
+ * @param options - Middleware options
151
+ * @returns Express RequestHandler
152
+ *
153
+ * @example
154
+ * ```typescript
155
+ * import express from "express";
156
+ * import { discoveryMiddleware } from "./discovery.js";
157
+ *
158
+ * const app = express();
159
+ *
160
+ * app.use(discoveryMiddleware({
161
+ * service: {
162
+ * name: "my-lafs-service",
163
+ * version: "1.0.0",
164
+ * description: "A LAFS-compliant API service"
165
+ * },
166
+ * capabilities: [
167
+ * {
168
+ * name: "envelope-processor",
169
+ * version: "1.0.0",
170
+ * operations: ["process", "validate"],
171
+ * description: "Process LAFS envelopes"
172
+ * }
173
+ * ],
174
+ * endpoints: {
175
+ * envelope: "/api/v1/envelope",
176
+ * context: "/api/v1/context"
177
+ * }
178
+ * }));
179
+ * ```
180
+ */
181
+ export function discoveryMiddleware(config, options = {}) {
182
+ const path = options.path || "/.well-known/lafs.json";
183
+ const enableHead = options.enableHead !== false;
184
+ const enableEtag = options.enableEtag !== false;
185
+ const cacheMaxAge = config.cacheMaxAge || 3600;
186
+ // Validate configuration
187
+ if (!config.service?.name || !config.service?.version) {
188
+ throw new Error("Discovery config requires service.name and service.version");
189
+ }
190
+ if (!Array.isArray(config.capabilities)) {
191
+ throw new Error("Discovery config requires capabilities array");
192
+ }
193
+ if (!config.endpoints?.envelope) {
194
+ throw new Error("Discovery config requires endpoints.envelope");
195
+ }
196
+ return function discoveryHandler(req, res, next) {
197
+ // Only handle requests to the discovery path
198
+ if (req.path !== path) {
199
+ next();
200
+ return;
201
+ }
202
+ // Handle HEAD requests
203
+ if (req.method === "HEAD") {
204
+ if (!enableHead) {
205
+ res.status(405).json({
206
+ error: "Method Not Allowed",
207
+ message: "HEAD requests are disabled for this endpoint"
208
+ });
209
+ return;
210
+ }
211
+ // For HEAD, we still need to build the document to get the ETag
212
+ const doc = buildDiscoveryDocument(config, req);
213
+ const json = JSON.stringify(doc);
214
+ // Generate stable ETag from config hash (not request-dependent document)
215
+ const configHash = generateETag(JSON.stringify({
216
+ schemaUrl: config.schemaUrl,
217
+ lafsVersion: config.lafsVersion,
218
+ service: config.service,
219
+ capabilities: config.capabilities,
220
+ endpoints: config.endpoints,
221
+ cacheMaxAge: config.cacheMaxAge
222
+ }));
223
+ const etag = enableEtag ? configHash : undefined;
224
+ res.set({
225
+ "Content-Type": "application/json",
226
+ "Cache-Control": `public, max-age=${cacheMaxAge}`,
227
+ ...(etag && { "ETag": etag }),
228
+ "Content-Length": Buffer.byteLength(json)
229
+ });
230
+ res.status(200).end();
231
+ return;
232
+ }
233
+ // Only handle GET requests
234
+ if (req.method !== "GET") {
235
+ res.status(405).json({
236
+ error: "Method Not Allowed",
237
+ message: `Method ${req.method} not allowed. Use GET or HEAD.`
238
+ });
239
+ return;
240
+ }
241
+ try {
242
+ // Build discovery document
243
+ const doc = buildDiscoveryDocument(config, req);
244
+ // Validate against schema
245
+ validateDocument(doc);
246
+ // Serialize document
247
+ const json = JSON.stringify(doc);
248
+ // Generate ETag from config hash (stable) rather than request-dependent document
249
+ // This ensures ETag is consistent across requests even when URLs are constructed from request
250
+ const configHash = generateETag(JSON.stringify({
251
+ schemaUrl: config.schemaUrl,
252
+ lafsVersion: config.lafsVersion,
253
+ service: config.service,
254
+ capabilities: config.capabilities,
255
+ endpoints: config.endpoints,
256
+ cacheMaxAge: config.cacheMaxAge
257
+ }));
258
+ const etag = enableEtag ? configHash : undefined;
259
+ // Check If-None-Match for conditional request
260
+ if (enableEtag && req.headers["if-none-match"] === etag) {
261
+ res.status(304).end();
262
+ return;
263
+ }
264
+ // Set response headers
265
+ const headers = {
266
+ "Content-Type": "application/json",
267
+ "Cache-Control": `public, max-age=${cacheMaxAge}`,
268
+ ...config.headers
269
+ };
270
+ if (etag) {
271
+ headers["ETag"] = etag;
272
+ }
273
+ res.set(headers);
274
+ res.status(200).send(json);
275
+ }
276
+ catch (error) {
277
+ next(error);
278
+ }
279
+ };
280
+ }
281
+ /**
282
+ * Fastify plugin for LAFS discovery (for Fastify users)
283
+ *
284
+ * @param fastify - Fastify instance
285
+ * @param options - Plugin options
286
+ */
287
+ export async function discoveryFastifyPlugin(fastify, options) {
288
+ const path = options.path || "/.well-known/lafs.json";
289
+ const config = options.config;
290
+ const cacheMaxAge = config.cacheMaxAge || 3600;
291
+ const handler = async (request, reply) => {
292
+ const doc = buildDiscoveryDocument(config, request.raw);
293
+ validateDocument(doc);
294
+ const json = JSON.stringify(doc);
295
+ const etag = generateETag(json);
296
+ reply.header("Content-Type", "application/json");
297
+ reply.header("Cache-Control", `public, max-age=${cacheMaxAge}`);
298
+ reply.header("ETag", etag);
299
+ return doc;
300
+ };
301
+ // Note: Actual route registration depends on Fastify's API
302
+ // This is a type-safe signature for the plugin
303
+ }
304
+ export default discoveryMiddleware;
File without changes
File without changes
File without changes
File without changes