@forwardimpact/librpc 0.1.87 → 0.1.89

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/package.json CHANGED
@@ -1,14 +1,24 @@
1
1
  {
2
2
  "name": "@forwardimpact/librpc",
3
- "version": "0.1.87",
3
+ "version": "0.1.89",
4
4
  "description": "gRPC framework and utilities for Guide",
5
5
  "license": "Apache-2.0",
6
6
  "author": "D. Olsson <hi@senzilla.io>",
7
7
  "type": "module",
8
- "main": "index.js",
8
+ "main": "./src/index.js",
9
+ "exports": {
10
+ ".": "./src/index.js",
11
+ "./client.js": "./src/client.js",
12
+ "./base.js": "./src/base.js"
13
+ },
9
14
  "bin": {
10
15
  "fit-unary": "./bin/fit-unary.js"
11
16
  },
17
+ "files": [
18
+ "src/**/*.js",
19
+ "bin/**/*.js",
20
+ "README.md"
21
+ ],
12
22
  "engines": {
13
23
  "bun": ">=1.2.0",
14
24
  "node": ">=18.0.0"
package/src/health.js ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * gRPC Health Check protocol (grpc.health.v1.Health/Check)
3
+ *
4
+ * Manual service definition — no .proto file or proto-loader needed.
5
+ * Messages use raw protobuf wire encoding (trivial: one string field,
6
+ * one enum/varint field).
7
+ */
8
+
9
+ /** Serving status enum matching grpc.health.v1 */
10
+ export const ServingStatus = {
11
+ UNKNOWN: 0,
12
+ SERVING: 1,
13
+ NOT_SERVING: 2,
14
+ SERVICE_UNKNOWN: 3,
15
+ };
16
+
17
+ /**
18
+ * Serialize a HealthCheckRequest to bytes.
19
+ * Proto: message HealthCheckRequest { string service = 1; }
20
+ */
21
+ function serializeRequest(value) {
22
+ const service = value?.service || "";
23
+ if (service.length === 0) return Buffer.alloc(0);
24
+ const encoded = Buffer.from(service, "utf8");
25
+ // field 1, wire type 2 (length-delimited): tag = 0x0a
26
+ const header = Buffer.alloc(2);
27
+ header[0] = 0x0a;
28
+ header[1] = encoded.length; // works for lengths < 128
29
+ return Buffer.concat([header, encoded]);
30
+ }
31
+
32
+ /**
33
+ * Deserialize bytes to a HealthCheckRequest.
34
+ */
35
+ function deserializeRequest(buffer) {
36
+ if (!buffer || buffer.length === 0) return { service: "" };
37
+ if (buffer[0] === 0x0a) {
38
+ const len = buffer[1];
39
+ return { service: buffer.subarray(2, 2 + len).toString("utf8") };
40
+ }
41
+ return { service: "" };
42
+ }
43
+
44
+ /**
45
+ * Serialize a HealthCheckResponse to bytes.
46
+ * Proto: message HealthCheckResponse { ServingStatus status = 1; }
47
+ */
48
+ function serializeResponse(value) {
49
+ const status = value?.status ?? 0;
50
+ if (status === 0) return Buffer.alloc(0);
51
+ // field 1, wire type 0 (varint): tag = 0x08
52
+ return Buffer.from([0x08, status]);
53
+ }
54
+
55
+ /**
56
+ * Deserialize bytes to a HealthCheckResponse.
57
+ */
58
+ function deserializeResponse(buffer) {
59
+ if (!buffer || buffer.length === 0) return { status: 0 };
60
+ if (buffer[0] === 0x08) return { status: buffer[1] };
61
+ return { status: 0 };
62
+ }
63
+
64
+ /** Service definition compatible with grpc.Server.addService */
65
+ export const healthDefinition = {
66
+ Check: {
67
+ path: "/grpc.health.v1.Health/Check",
68
+ requestStream: false,
69
+ responseStream: false,
70
+ requestSerialize: serializeRequest,
71
+ requestDeserialize: deserializeRequest,
72
+ responseSerialize: serializeResponse,
73
+ responseDeserialize: deserializeResponse,
74
+ },
75
+ };
76
+
77
+ /**
78
+ * Creates health check handlers for the given service name.
79
+ * @param {string} serviceName - The application service name (e.g., "Graph")
80
+ * @returns {object} Handler map for the Health service definition
81
+ */
82
+ export function createHealthHandlers(serviceName) {
83
+ return {
84
+ Check: (_call, callback) => {
85
+ const requestedService = _call.request?.service || "";
86
+
87
+ if (requestedService === "" || requestedService === serviceName) {
88
+ return callback(null, { status: ServingStatus.SERVING });
89
+ }
90
+ return callback(null, { status: ServingStatus.SERVICE_UNKNOWN });
91
+ },
92
+ };
93
+ }
@@ -10,6 +10,11 @@ export { createGrpc, createAuth, Rpc } from "./base.js";
10
10
  export { Client } from "./client.js";
11
11
  export { Interceptor, HmacAuth } from "./auth.js";
12
12
  export { Server } from "./server.js";
13
+ export {
14
+ healthDefinition,
15
+ createHealthHandlers,
16
+ ServingStatus,
17
+ } from "./health.js";
13
18
 
14
19
  // Export services and clients objects for runtime access
15
20
  export const services = exports.services || {};
@@ -5,6 +5,7 @@ import {
5
5
  createObserver,
6
6
  capitalizeFirstLetter,
7
7
  } from "./base.js";
8
+ import { healthDefinition, createHealthHandlers } from "./health.js";
8
9
 
9
10
  /**
10
11
  * gRPC Server class using pre-compiled service definitions
@@ -63,6 +64,12 @@ export class Server extends Rpc {
63
64
 
64
65
  this.#server.addService(definition, wrappedHandlers);
65
66
 
67
+ // Register standard gRPC health check (no auth, no observer wrapping)
68
+ this.#server.addService(
69
+ healthDefinition,
70
+ createHealthHandlers(serviceName),
71
+ );
72
+
66
73
  const uri = `${this.config.host}:${this.config.port}`;
67
74
  await this.#bindServer(uri);
68
75
 
package/test/auth.test.js DELETED
@@ -1,94 +0,0 @@
1
- import { test, describe, beforeEach } from "node:test";
2
- import assert from "node:assert";
3
-
4
- import { createAuth, HmacAuth } from "../index.js";
5
-
6
- describe("Auth", () => {
7
- describe("HmacAuth", () => {
8
- test("should throw if secret is missing or too short", () => {
9
- assert.throws(() => new HmacAuth(), /Secret must be a non-empty string/);
10
- assert.throws(
11
- () => new HmacAuth("short"),
12
- /Secret must be at least 32 characters long/,
13
- );
14
- });
15
-
16
- test("should generate and verify valid tokens", () => {
17
- const secret = "test-secret-that-is-at-least-32-characters-long";
18
- const auth = new HmacAuth(secret);
19
- const serviceId = "test-service";
20
-
21
- const token = auth.generateToken(serviceId);
22
- assert.ok(token);
23
-
24
- const result = auth.verifyToken(token);
25
- assert.strictEqual(result.isValid, true);
26
- assert.strictEqual(result.serviceId, serviceId);
27
- });
28
-
29
- test("should reject expired tokens", async () => {
30
- const secret = "test-secret-that-is-at-least-32-characters-long";
31
- // 1 second lifetime
32
- const auth = new HmacAuth(secret, 1);
33
- const serviceId = "test-service";
34
-
35
- const token = auth.generateToken(serviceId);
36
-
37
- // Wait for token to expire (1.1 seconds)
38
- await new Promise((resolve) => setTimeout(resolve, 1100));
39
-
40
- const result = auth.verifyToken(token);
41
- assert.strictEqual(result.isValid, false);
42
- assert.match(result.error, /Token has expired/);
43
- });
44
-
45
- test("should reject invalid signatures", () => {
46
- const secret = "test-secret-that-is-at-least-32-characters-long";
47
- const auth = new HmacAuth(secret);
48
- const serviceId = "test-service";
49
-
50
- const token = auth.generateToken(serviceId);
51
-
52
- // Tamper with the token (it's base64 encoded)
53
- const decoded = Buffer.from(token, "base64").toString("utf8");
54
- const parts = decoded.split(":");
55
- // Change serviceId part
56
- parts[0] = "other-service";
57
- const tamperedToken = Buffer.from(parts.join(":")).toString("base64");
58
-
59
- const result = auth.verifyToken(tamperedToken);
60
- assert.strictEqual(result.isValid, false);
61
- assert.match(result.error, /Invalid token signature/);
62
- });
63
- });
64
-
65
- describe("createAuth", () => {
66
- let originalEnv;
67
-
68
- beforeEach(() => {
69
- originalEnv = process.env.SERVICE_SECRET;
70
- });
71
-
72
- test("should throw if SERVICE_SECRET is missing", () => {
73
- delete process.env.SERVICE_SECRET;
74
- assert.throws(
75
- () => createAuth("test"),
76
- /SERVICE_SECRET environment variable is required/,
77
- );
78
- });
79
-
80
- test("should create Interceptor with valid secret", () => {
81
- process.env.SERVICE_SECRET =
82
- "test-secret-that-is-at-least-32-characters-long";
83
- const interceptor = createAuth("test");
84
- assert.ok(interceptor);
85
-
86
- // Cleanup
87
- if (originalEnv) {
88
- process.env.SERVICE_SECRET = originalEnv;
89
- } else {
90
- delete process.env.SERVICE_SECRET;
91
- }
92
- });
93
- });
94
- });
@@ -1,138 +0,0 @@
1
- import { test, describe, beforeEach, mock } from "node:test";
2
- import assert from "node:assert";
3
- import { PassThrough } from "stream";
4
-
5
- import { Client } from "../index.js";
6
- import {
7
- createMockGrpcFn,
8
- createMockObserverFn,
9
- createMockAuthFn,
10
- createMockLogger,
11
- } from "@forwardimpact/libharness";
12
-
13
- describe("Client", () => {
14
- let mockConfig;
15
- let mockGrpcFn;
16
- let mockAuthFn;
17
- let mockLogFn;
18
- let mockObserverFn;
19
- let mockClientInstance;
20
-
21
- beforeEach(() => {
22
- mockConfig = {
23
- name: "memory", // Use a valid service name that exists in definitions
24
- host: "0.0.0.0",
25
- port: 5000,
26
- };
27
-
28
- mockClientInstance = {
29
- TestMethod: mock.fn((_req, _meta, cb) => cb(null, { result: "success" })),
30
- StreamMethod: mock.fn((_req, _meta) => {
31
- const stream = new PassThrough({ objectMode: true });
32
- process.nextTick(() => {
33
- stream.emit("metadata", {});
34
- stream.write({ result: "chunk1" });
35
- stream.write({ result: "chunk2" });
36
- stream.end();
37
- });
38
- return stream;
39
- }),
40
- };
41
-
42
- mockGrpcFn = createMockGrpcFn({
43
- grpc: {
44
- Metadata: class {},
45
- makeGenericClientConstructor: mock.fn(() => {
46
- return function () {
47
- return mockClientInstance;
48
- };
49
- }),
50
- },
51
- });
52
-
53
- mockAuthFn = createMockAuthFn();
54
-
55
- mockLogFn = createMockLogger();
56
-
57
- mockObserverFn = createMockObserverFn();
58
- });
59
-
60
- test("should require config parameter", () => {
61
- assert.throws(
62
- () =>
63
- new Client(
64
- null,
65
- mockLogFn,
66
- null,
67
- mockObserverFn,
68
- mockGrpcFn,
69
- mockAuthFn,
70
- ),
71
- /config is required/,
72
- );
73
- });
74
-
75
- test("should accept valid parameters", () => {
76
- const client = new Client(
77
- mockConfig,
78
- mockLogFn,
79
- null,
80
- mockObserverFn,
81
- mockGrpcFn,
82
- mockAuthFn,
83
- );
84
-
85
- assert.ok(client);
86
- assert.strictEqual(client.config, mockConfig);
87
- });
88
-
89
- test("callUnary should execute unary call", async () => {
90
- const client = new Client(
91
- mockConfig,
92
- mockLogFn,
93
- null,
94
- mockObserverFn,
95
- mockGrpcFn,
96
- mockAuthFn,
97
- );
98
-
99
- const response = await client.callUnary("TestMethod", { some: "data" });
100
- assert.deepStrictEqual(response, { result: "success" });
101
- assert.strictEqual(mockClientInstance.TestMethod.mock.callCount(), 1);
102
- });
103
-
104
- test("callStream should execute streaming call", async () => {
105
- const client = new Client(
106
- mockConfig,
107
- mockLogFn,
108
- null,
109
- mockObserverFn,
110
- mockGrpcFn,
111
- mockAuthFn,
112
- );
113
-
114
- const stream = client.callStream("StreamMethod", { some: "data" });
115
-
116
- const chunks = [];
117
- for await (const chunk of stream) {
118
- chunks.push(chunk);
119
- }
120
-
121
- assert.strictEqual(chunks.length, 2);
122
- assert.deepStrictEqual(chunks[0], { result: "chunk1" });
123
- assert.deepStrictEqual(chunks[1], { result: "chunk2" });
124
- assert.strictEqual(mockClientInstance.StreamMethod.mock.callCount(), 1);
125
- });
126
-
127
- test("should accept tracer parameter", () => {
128
- const client = new Client(
129
- mockConfig,
130
- mockLogFn,
131
- {}, // tracer
132
- mockObserverFn,
133
- mockGrpcFn,
134
- mockAuthFn,
135
- );
136
- assert.ok(client);
137
- });
138
- });
@@ -1,123 +0,0 @@
1
- import { test, describe, beforeEach, mock } from "node:test";
2
- import assert from "node:assert";
3
-
4
- import { Server } from "../index.js";
5
- import {
6
- createMockConfig,
7
- createMockGrpcFn,
8
- createMockObserverFn,
9
- createMockAuthFn,
10
- createMockLogger,
11
- createMockTracer,
12
- } from "@forwardimpact/libharness";
13
-
14
- describe("Server", () => {
15
- let mockService;
16
- let mockConfig;
17
- let mockGrpcFn;
18
- let mockAuthFn;
19
- let mockLogFn;
20
- let mockObserverFn;
21
-
22
- beforeEach(() => {
23
- mockService = {
24
- getHandlers: () => ({
25
- TestMethod: async (_call) => ({ result: "test" }),
26
- }),
27
- };
28
-
29
- mockConfig = createMockConfig("memory", { host: "0.0.0.0", port: 5000 });
30
- mockGrpcFn = createMockGrpcFn();
31
- mockAuthFn = createMockAuthFn();
32
- mockLogFn = createMockLogger();
33
- mockObserverFn = createMockObserverFn(mockLogFn);
34
- });
35
-
36
- test("should require service parameter", () => {
37
- assert.throws(
38
- () =>
39
- new Server(
40
- null,
41
- mockConfig,
42
- mockLogFn,
43
- null,
44
- mockObserverFn,
45
- mockGrpcFn,
46
- mockAuthFn,
47
- ),
48
- /service is required/,
49
- );
50
- });
51
-
52
- test("should require config parameter", () => {
53
- assert.throws(
54
- () =>
55
- new Server(
56
- mockService,
57
- null,
58
- mockLogFn,
59
- null,
60
- mockObserverFn,
61
- mockGrpcFn,
62
- mockAuthFn,
63
- ),
64
- /config is required/,
65
- );
66
- });
67
-
68
- test("should accept valid parameters", () => {
69
- const server = new Server(
70
- mockService,
71
- mockConfig,
72
- mockLogFn,
73
- null,
74
- mockObserverFn,
75
- mockGrpcFn,
76
- mockAuthFn,
77
- );
78
-
79
- assert.ok(server);
80
- assert.strictEqual(server.config, mockConfig);
81
- });
82
-
83
- test("should call service methods during setup", async () => {
84
- const getHandlersSpy = mock.fn(() => ({
85
- TestMethod: async () => ({ result: "test" }),
86
- }));
87
-
88
- const spiedService = {
89
- getHandlers: getHandlersSpy,
90
- };
91
-
92
- const server = new Server(
93
- spiedService,
94
- mockConfig,
95
- mockLogFn,
96
- null,
97
- mockObserverFn,
98
- mockGrpcFn,
99
- mockAuthFn,
100
- );
101
-
102
- // Start the server to trigger setup
103
- await server.start();
104
-
105
- assert.strictEqual(getHandlersSpy.mock.callCount(), 1);
106
- });
107
-
108
- test("should accept tracer parameter", () => {
109
- const mockTracer = createMockTracer();
110
-
111
- const server = new Server(
112
- mockService,
113
- mockConfig,
114
- mockLogFn,
115
- mockTracer,
116
- mockObserverFn,
117
- mockGrpcFn,
118
- mockAuthFn,
119
- );
120
-
121
- assert.ok(server);
122
- });
123
- });
File without changes
File without changes
File without changes
File without changes