@zero-server/sdk 0.9.1 → 0.9.3
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/LICENSE +21 -21
- package/README.md +460 -443
- package/index.js +414 -412
- package/lib/app.js +1172 -1172
- package/lib/auth/authorize.js +399 -399
- package/lib/auth/enrollment.js +367 -367
- package/lib/auth/index.js +57 -57
- package/lib/auth/jwt.js +731 -731
- package/lib/auth/oauth.js +362 -362
- package/lib/auth/session.js +588 -588
- package/lib/auth/trustedDevice.js +409 -409
- package/lib/auth/twoFactor.js +1150 -1150
- package/lib/auth/webauthn.js +946 -946
- package/lib/body/index.js +14 -14
- package/lib/body/json.js +109 -109
- package/lib/body/multipart.js +440 -440
- package/lib/body/raw.js +71 -71
- package/lib/body/rawBuffer.js +160 -160
- package/lib/body/sendError.js +25 -25
- package/lib/body/text.js +75 -75
- package/lib/body/typeMatch.js +41 -41
- package/lib/body/urlencoded.js +235 -235
- package/lib/cli.js +845 -845
- package/lib/cluster.js +666 -666
- package/lib/debug.js +372 -372
- package/lib/env/index.js +465 -465
- package/lib/errors.js +683 -683
- package/lib/fetch/index.js +256 -256
- package/lib/grpc/balancer.js +378 -378
- package/lib/grpc/call.js +708 -708
- package/lib/grpc/client.js +764 -764
- package/lib/grpc/codec.js +1221 -1221
- package/lib/grpc/credentials.js +398 -398
- package/lib/grpc/frame.js +262 -262
- package/lib/grpc/health.js +287 -287
- package/lib/grpc/index.js +121 -121
- package/lib/grpc/metadata.js +461 -461
- package/lib/grpc/proto.js +821 -821
- package/lib/grpc/reflection.js +590 -590
- package/lib/grpc/server.js +445 -445
- package/lib/grpc/status.js +118 -118
- package/lib/grpc/watch.js +173 -173
- package/lib/http/index.js +10 -10
- package/lib/http/request.js +727 -727
- package/lib/http/response.js +799 -799
- package/lib/lifecycle.js +557 -557
- package/lib/middleware/compress.js +230 -230
- package/lib/middleware/cookieParser.js +237 -237
- package/lib/middleware/cors.js +93 -93
- package/lib/middleware/csrf.js +137 -137
- package/lib/middleware/errorHandler.js +101 -101
- package/lib/middleware/helmet.js +175 -175
- package/lib/middleware/index.js +19 -17
- package/lib/middleware/logger.js +74 -74
- package/lib/middleware/rateLimit.js +88 -88
- package/lib/middleware/requestId.js +53 -53
- package/lib/middleware/static.js +326 -326
- package/lib/middleware/timeout.js +71 -71
- package/lib/middleware/validator.js +255 -255
- package/lib/observe/health.js +326 -326
- package/lib/observe/index.js +50 -50
- package/lib/observe/logger.js +359 -359
- package/lib/observe/metrics.js +805 -805
- package/lib/observe/tracing.js +592 -592
- package/lib/orm/adapters/json.js +290 -290
- package/lib/orm/adapters/memory.js +764 -764
- package/lib/orm/adapters/mongo.js +764 -764
- package/lib/orm/adapters/mysql.js +933 -933
- package/lib/orm/adapters/postgres.js +1144 -1144
- package/lib/orm/adapters/redis.js +1534 -1534
- package/lib/orm/adapters/sql-base.js +212 -212
- package/lib/orm/adapters/sqlite.js +858 -858
- package/lib/orm/audit.js +649 -649
- package/lib/orm/cache.js +394 -394
- package/lib/orm/geo.js +387 -387
- package/lib/orm/index.js +784 -784
- package/lib/orm/migrate.js +432 -432
- package/lib/orm/model.js +1706 -1706
- package/lib/orm/plugin.js +375 -375
- package/lib/orm/procedures.js +836 -836
- package/lib/orm/profiler.js +233 -233
- package/lib/orm/query.js +1772 -1772
- package/lib/orm/replicas.js +241 -241
- package/lib/orm/schema.js +307 -307
- package/lib/orm/search.js +380 -380
- package/lib/orm/seed/data/commerce.js +136 -136
- package/lib/orm/seed/data/internet.js +111 -111
- package/lib/orm/seed/data/locations.js +204 -204
- package/lib/orm/seed/data/names.js +338 -338
- package/lib/orm/seed/data/person.js +128 -128
- package/lib/orm/seed/data/phone.js +211 -211
- package/lib/orm/seed/data/words.js +134 -134
- package/lib/orm/seed/factory.js +178 -178
- package/lib/orm/seed/fake.js +1186 -1186
- package/lib/orm/seed/index.js +18 -18
- package/lib/orm/seed/rng.js +70 -70
- package/lib/orm/seed/seeder.js +124 -124
- package/lib/orm/seed/unique.js +68 -68
- package/lib/orm/snapshot.js +366 -366
- package/lib/orm/tenancy.js +605 -605
- package/lib/orm/views.js +350 -350
- package/lib/router/index.js +436 -436
- package/lib/sse/index.js +8 -8
- package/lib/sse/stream.js +349 -349
- package/lib/ws/connection.js +451 -451
- package/lib/ws/handshake.js +125 -125
- package/lib/ws/index.js +14 -14
- package/lib/ws/room.js +223 -223
- package/package.json +73 -73
- package/types/app.d.ts +223 -223
- package/types/auth.d.ts +520 -520
- package/types/body.d.ts +14 -0
- package/types/cli.d.ts +2 -0
- package/types/cluster.d.ts +75 -75
- package/types/env.d.ts +80 -80
- package/types/errors.d.ts +316 -316
- package/types/fetch.d.ts +43 -43
- package/types/grpc.d.ts +432 -432
- package/types/index.d.ts +384 -384
- package/types/lifecycle.d.ts +60 -60
- package/types/middleware.d.ts +320 -320
- package/types/observe.d.ts +304 -304
- package/types/orm.d.ts +1887 -1887
- package/types/request.d.ts +109 -109
- package/types/response.d.ts +157 -157
- package/types/router.d.ts +78 -78
- package/types/sse.d.ts +78 -78
- package/types/websocket.d.ts +126 -126
package/lib/grpc/reflection.js
CHANGED
|
@@ -1,590 +1,590 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module grpc/reflection
|
|
3
|
-
* @description gRPC Server Reflection Protocol implementation (grpc.reflection.v1).
|
|
4
|
-
* Enables `grpcurl`, `grpcui`, Postman, and other debugging tools to
|
|
5
|
-
* introspect registered services without supplying `.proto` files.
|
|
6
|
-
*
|
|
7
|
-
* @see https://github.com/grpc/grpc/blob/master/doc/server-reflection.md
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* const app = createApp();
|
|
11
|
-
* app.grpc(schema, 'Greeter', handlers);
|
|
12
|
-
* app.grpcReflection(); // dev-only by default
|
|
13
|
-
* app.listen(50051, { http2: true });
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
const log = require('../debug')('zero:grpc:reflection');
|
|
17
|
-
const { GrpcStatus } = require('./status');
|
|
18
|
-
const { encode, decode } = require('./codec');
|
|
19
|
-
const { Writer } = require('./codec');
|
|
20
|
-
|
|
21
|
-
// -- Protobuf field type numbers (google.protobuf.FieldDescriptorProto.Type) --
|
|
22
|
-
|
|
23
|
-
const PB_TYPE = {
|
|
24
|
-
double: 1, float: 2, int64: 3, uint64: 4, int32: 5,
|
|
25
|
-
fixed64: 6, fixed32: 7, bool: 8, string: 9, group: 10,
|
|
26
|
-
message: 11, bytes: 12, uint32: 13, enum: 14, sfixed32: 15,
|
|
27
|
-
sfixed64: 16, sint32: 17, sint64: 18,
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const PB_LABEL = { optional: 1, required: 2, repeated: 3 };
|
|
31
|
-
|
|
32
|
-
// -- FileDescriptorProto Builder (protobuf self-description) ---
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Build a serialized FileDescriptorProto from a parsed proto schema.
|
|
36
|
-
* This is the binary format returned by the reflection service.
|
|
37
|
-
*
|
|
38
|
-
* We hand-construct the protobuf wire format directly since we can't
|
|
39
|
-
* use the codec (which needs descriptors that we're building).
|
|
40
|
-
* Uses the Writer class for encoding.
|
|
41
|
-
*
|
|
42
|
-
* @private
|
|
43
|
-
* @param {object} schema - Parsed proto schema.
|
|
44
|
-
* @param {string} [filename] - File name for the descriptor.
|
|
45
|
-
* @returns {Buffer} Serialized FileDescriptorProto.
|
|
46
|
-
*/
|
|
47
|
-
function buildFileDescriptorProto(schema, filename)
|
|
48
|
-
{
|
|
49
|
-
const w = new Writer();
|
|
50
|
-
|
|
51
|
-
// field 1: name (string)
|
|
52
|
-
if (filename) w.string(1, filename);
|
|
53
|
-
|
|
54
|
-
// field 2: package (string)
|
|
55
|
-
if (schema.package) w.string(2, schema.package);
|
|
56
|
-
|
|
57
|
-
// field 3: dependency (repeated string) — imported file names
|
|
58
|
-
if (schema.imports)
|
|
59
|
-
{
|
|
60
|
-
for (const imp of schema.imports)
|
|
61
|
-
w.string(3, imp.path);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// field 4: message_type (repeated DescriptorProto)
|
|
65
|
-
for (const [name, msg] of Object.entries(schema.messages))
|
|
66
|
-
{
|
|
67
|
-
// Only emit top-level messages (skip nested ones that were flattened)
|
|
68
|
-
if (name.includes('.') && !name.startsWith(schema.package)) continue;
|
|
69
|
-
// Skip if it's a duplicate from flattening (name has package prefix)
|
|
70
|
-
const shortName = name.includes('.') ? name.split('.').pop() : name;
|
|
71
|
-
if (shortName !== name && schema.messages[shortName] === msg) continue;
|
|
72
|
-
|
|
73
|
-
const msgBytes = _buildDescriptorProto(msg, schema);
|
|
74
|
-
w.bytes(4, msgBytes);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// field 5: enum_type (repeated EnumDescriptorProto)
|
|
78
|
-
for (const [name, enumDef] of Object.entries(schema.enums))
|
|
79
|
-
{
|
|
80
|
-
if (name.includes('.')) continue; // skip flattened duplicates
|
|
81
|
-
const enumBytes = _buildEnumDescriptorProto(enumDef);
|
|
82
|
-
w.bytes(5, enumBytes);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// field 6: service (repeated ServiceDescriptorProto)
|
|
86
|
-
for (const [name, svc] of Object.entries(schema.services))
|
|
87
|
-
{
|
|
88
|
-
const svcBytes = _buildServiceDescriptorProto(svc, schema);
|
|
89
|
-
w.bytes(6, svcBytes);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// field 12: syntax (string)
|
|
93
|
-
if (schema.syntax) w.string(12, schema.syntax);
|
|
94
|
-
|
|
95
|
-
return w.finish();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Build a DescriptorProto (message descriptor).
|
|
100
|
-
* @private
|
|
101
|
-
*/
|
|
102
|
-
function _buildDescriptorProto(msg, schema)
|
|
103
|
-
{
|
|
104
|
-
const w = new Writer();
|
|
105
|
-
|
|
106
|
-
// field 1: name
|
|
107
|
-
w.string(1, msg.name);
|
|
108
|
-
|
|
109
|
-
// field 2: field (repeated FieldDescriptorProto)
|
|
110
|
-
if (msg.fields)
|
|
111
|
-
{
|
|
112
|
-
for (const field of msg.fields)
|
|
113
|
-
{
|
|
114
|
-
if (field.map)
|
|
115
|
-
{
|
|
116
|
-
const fieldBytes = _buildMapFieldDescriptor(field, schema);
|
|
117
|
-
w.bytes(2, fieldBytes);
|
|
118
|
-
}
|
|
119
|
-
else
|
|
120
|
-
{
|
|
121
|
-
const fieldBytes = _buildFieldDescriptorProto(field, schema);
|
|
122
|
-
w.bytes(2, fieldBytes);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// field 3: nested_type (repeated DescriptorProto)
|
|
128
|
-
if (msg.nested)
|
|
129
|
-
{
|
|
130
|
-
for (const nested of Object.values(msg.nested))
|
|
131
|
-
{
|
|
132
|
-
w.bytes(3, _buildDescriptorProto(nested, schema));
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// field 4: enum_type (repeated EnumDescriptorProto)
|
|
137
|
-
if (msg.nestedEnums)
|
|
138
|
-
{
|
|
139
|
-
for (const nested of Object.values(msg.nestedEnums))
|
|
140
|
-
{
|
|
141
|
-
w.bytes(4, _buildEnumDescriptorProto(nested));
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// field 8: oneof_decl (repeated OneofDescriptorProto)
|
|
146
|
-
if (msg.oneofs)
|
|
147
|
-
{
|
|
148
|
-
let idx = 0;
|
|
149
|
-
const oneofIndices = {};
|
|
150
|
-
for (const oneofName of Object.keys(msg.oneofs))
|
|
151
|
-
{
|
|
152
|
-
const ow = new Writer();
|
|
153
|
-
ow.string(1, oneofName);
|
|
154
|
-
w.bytes(8, ow.finish());
|
|
155
|
-
oneofIndices[oneofName] = idx++;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return w.finish();
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Build a FieldDescriptorProto.
|
|
164
|
-
* @private
|
|
165
|
-
*/
|
|
166
|
-
function _buildFieldDescriptorProto(field, schema)
|
|
167
|
-
{
|
|
168
|
-
const w = new Writer();
|
|
169
|
-
|
|
170
|
-
// field 1: name
|
|
171
|
-
w.string(1, field.name);
|
|
172
|
-
|
|
173
|
-
// field 3: number
|
|
174
|
-
w.int32(3, field.number);
|
|
175
|
-
|
|
176
|
-
// field 4: label
|
|
177
|
-
if (field.repeated) w.int32(4, PB_LABEL.repeated);
|
|
178
|
-
else w.int32(4, PB_LABEL.optional);
|
|
179
|
-
|
|
180
|
-
// field 5: type
|
|
181
|
-
const pbType = _resolveFieldType(field, schema);
|
|
182
|
-
w.int32(5, pbType);
|
|
183
|
-
|
|
184
|
-
// field 6: type_name (for message and enum types)
|
|
185
|
-
if (pbType === PB_TYPE.message || pbType === PB_TYPE.enum)
|
|
186
|
-
{
|
|
187
|
-
const typeName = field.type.startsWith('.') ? field.type : '.' + (schema.package ? schema.package + '.' : '') + field.type;
|
|
188
|
-
w.string(6, typeName);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// field 9: oneof_index (if in a oneof)
|
|
192
|
-
if (field.oneofName !== undefined)
|
|
193
|
-
{
|
|
194
|
-
// Will use the index from the parent — simplified to 0 for now
|
|
195
|
-
w.int32(9, 0);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return w.finish();
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Build a map field descriptor (as a repeated message with map_entry option).
|
|
203
|
-
* @private
|
|
204
|
-
*/
|
|
205
|
-
function _buildMapFieldDescriptor(field, schema)
|
|
206
|
-
{
|
|
207
|
-
const w = new Writer();
|
|
208
|
-
w.string(1, field.name);
|
|
209
|
-
w.int32(3, field.number);
|
|
210
|
-
w.int32(4, PB_LABEL.repeated);
|
|
211
|
-
w.int32(5, PB_TYPE.message);
|
|
212
|
-
// type_name for the auto-generated entry type
|
|
213
|
-
const entryName = field.name.charAt(0).toUpperCase() + field.name.slice(1) + 'Entry';
|
|
214
|
-
w.string(6, '.' + (schema.package ? schema.package + '.' : '') + entryName);
|
|
215
|
-
return w.finish();
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Build an EnumDescriptorProto.
|
|
220
|
-
* @private
|
|
221
|
-
*/
|
|
222
|
-
function _buildEnumDescriptorProto(enumDef)
|
|
223
|
-
{
|
|
224
|
-
const w = new Writer();
|
|
225
|
-
|
|
226
|
-
// field 1: name
|
|
227
|
-
w.string(1, enumDef.name);
|
|
228
|
-
|
|
229
|
-
// field 2: value (repeated EnumValueDescriptorProto)
|
|
230
|
-
if (enumDef.values)
|
|
231
|
-
{
|
|
232
|
-
for (const [name, number] of Object.entries(enumDef.values))
|
|
233
|
-
{
|
|
234
|
-
const vw = new Writer();
|
|
235
|
-
vw.string(1, name);
|
|
236
|
-
vw.int32(2, number);
|
|
237
|
-
w.bytes(2, vw.finish());
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return w.finish();
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Build a ServiceDescriptorProto.
|
|
246
|
-
* @private
|
|
247
|
-
*/
|
|
248
|
-
function _buildServiceDescriptorProto(svc, schema)
|
|
249
|
-
{
|
|
250
|
-
const w = new Writer();
|
|
251
|
-
|
|
252
|
-
// field 1: name
|
|
253
|
-
w.string(1, svc.name);
|
|
254
|
-
|
|
255
|
-
// field 2: method (repeated MethodDescriptorProto)
|
|
256
|
-
for (const method of Object.values(svc.methods))
|
|
257
|
-
{
|
|
258
|
-
const mw = new Writer();
|
|
259
|
-
mw.string(1, method.name);
|
|
260
|
-
// input type (fully qualified)
|
|
261
|
-
mw.string(2, '.' + (schema.package ? schema.package + '.' : '') + method.inputType);
|
|
262
|
-
// output type
|
|
263
|
-
mw.string(3, '.' + (schema.package ? schema.package + '.' : '') + method.outputType);
|
|
264
|
-
// client_streaming
|
|
265
|
-
if (method.clientStreaming) mw.bool(5, true);
|
|
266
|
-
// server_streaming
|
|
267
|
-
if (method.serverStreaming) mw.bool(6, true);
|
|
268
|
-
|
|
269
|
-
w.bytes(2, mw.finish());
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return w.finish();
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Resolve a field type string to protobuf type number.
|
|
277
|
-
* @private
|
|
278
|
-
*/
|
|
279
|
-
function _resolveFieldType(field, schema)
|
|
280
|
-
{
|
|
281
|
-
if (PB_TYPE[field.type]) return PB_TYPE[field.type];
|
|
282
|
-
if (schema.enums && schema.enums[field.type]) return PB_TYPE.enum;
|
|
283
|
-
if (schema.messages && schema.messages[field.type]) return PB_TYPE.message;
|
|
284
|
-
// Default to message type for unknown types (custom messages)
|
|
285
|
-
return PB_TYPE.message;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// -- Reflection Request/Response Descriptors ----------------
|
|
289
|
-
|
|
290
|
-
const _reflectionRequestDesc = {
|
|
291
|
-
name: 'ServerReflectionRequest',
|
|
292
|
-
fields: [
|
|
293
|
-
{ name: 'host', type: 'string', number: 1, repeated: false, optional: false, map: false },
|
|
294
|
-
{ name: 'file_by_filename', type: 'string', number: 3, repeated: false, optional: false, map: false, oneofName: 'message_request' },
|
|
295
|
-
{ name: 'file_containing_symbol', type: 'string', number: 4, repeated: false, optional: false, map: false, oneofName: 'message_request' },
|
|
296
|
-
{ name: 'list_services', type: 'string', number: 7, repeated: false, optional: false, map: false, oneofName: 'message_request' },
|
|
297
|
-
],
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
const _reflectionResponseDesc = {
|
|
301
|
-
name: 'ServerReflectionResponse',
|
|
302
|
-
fields: [
|
|
303
|
-
{ name: 'valid_host', type: 'string', number: 1, repeated: false, optional: false, map: false },
|
|
304
|
-
{ name: 'original_request', type: 'ServerReflectionRequest', number: 2, repeated: false, optional: false, map: false },
|
|
305
|
-
{ name: 'file_descriptor_response', type: 'FileDescriptorResponse', number: 4, repeated: false, optional: false, map: false, oneofName: 'message_response' },
|
|
306
|
-
{ name: 'all_extension_numbers_response', type: 'ExtensionNumberResponse', number: 5, repeated: false, optional: false, map: false, oneofName: 'message_response' },
|
|
307
|
-
{ name: 'list_services_response', type: 'ListServiceResponse', number: 6, repeated: false, optional: false, map: false, oneofName: 'message_response' },
|
|
308
|
-
{ name: 'error_response', type: 'ErrorResponse', number: 7, repeated: false, optional: false, map: false, oneofName: 'message_response' },
|
|
309
|
-
],
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
const _fileDescriptorResponseDesc = {
|
|
313
|
-
name: 'FileDescriptorResponse',
|
|
314
|
-
fields: [
|
|
315
|
-
{ name: 'file_descriptor_proto', type: 'bytes', number: 1, repeated: true, optional: false, map: false },
|
|
316
|
-
],
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
const _listServiceResponseDesc = {
|
|
320
|
-
name: 'ListServiceResponse',
|
|
321
|
-
fields: [
|
|
322
|
-
{ name: 'service', type: 'ServiceResponse', number: 1, repeated: true, optional: false, map: false },
|
|
323
|
-
],
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
const _serviceResponseDesc = {
|
|
327
|
-
name: 'ServiceResponse',
|
|
328
|
-
fields: [
|
|
329
|
-
{ name: 'name', type: 'string', number: 1, repeated: false, optional: false, map: false },
|
|
330
|
-
],
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
const _errorResponseDesc = {
|
|
334
|
-
name: 'ErrorResponse',
|
|
335
|
-
fields: [
|
|
336
|
-
{ name: 'error_code', type: 'int32', number: 1, repeated: false, optional: false, map: false },
|
|
337
|
-
{ name: 'error_message', type: 'string', number: 2, repeated: false, optional: false, map: false },
|
|
338
|
-
],
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
const _reflectionMessages = {
|
|
342
|
-
ServerReflectionRequest: _reflectionRequestDesc,
|
|
343
|
-
ServerReflectionResponse: _reflectionResponseDesc,
|
|
344
|
-
FileDescriptorResponse: _fileDescriptorResponseDesc,
|
|
345
|
-
ListServiceResponse: _listServiceResponseDesc,
|
|
346
|
-
ServiceResponse: _serviceResponseDesc,
|
|
347
|
-
ErrorResponse: _errorResponseDesc,
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
// -- Reflection Service ----------------------------------------
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Server Reflection service implementation.
|
|
354
|
-
* Caches serialized descriptors at registration time.
|
|
355
|
-
*
|
|
356
|
-
* @class
|
|
357
|
-
*/
|
|
358
|
-
class ReflectionService
|
|
359
|
-
{
|
|
360
|
-
/**
|
|
361
|
-
* @param {object} [opts]
|
|
362
|
-
* @param {boolean} [opts.production=false] - Enable in production.
|
|
363
|
-
*/
|
|
364
|
-
constructor(opts = {})
|
|
365
|
-
{
|
|
366
|
-
/** @private */
|
|
367
|
-
this._production = opts.production || false;
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Registered schemas indexed by filename.
|
|
371
|
-
* @type {Map<string, { schema: object, descriptor: Buffer }>}
|
|
372
|
-
*/
|
|
373
|
-
this._files = new Map();
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Symbol → filename mapping for file_containing_symbol lookups.
|
|
377
|
-
* @type {Map<string, string>}
|
|
378
|
-
*/
|
|
379
|
-
this._symbols = new Map();
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* All registered service names (fully qualified).
|
|
383
|
-
* @type {string[]}
|
|
384
|
-
*/
|
|
385
|
-
this._serviceNames = [];
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Register a schema for reflection.
|
|
390
|
-
* Builds and caches the FileDescriptorProto at registration time.
|
|
391
|
-
*
|
|
392
|
-
* @param {object} schema - Parsed proto schema.
|
|
393
|
-
* @param {string} [filename] - Filename to use. Defaults to schema package or '<inline>'.
|
|
394
|
-
*/
|
|
395
|
-
addSchema(schema, filename)
|
|
396
|
-
{
|
|
397
|
-
const fname = filename || (schema.package ? schema.package.replace(/\./g, '/') + '.proto' : '<inline>');
|
|
398
|
-
|
|
399
|
-
if (this._files.has(fname)) return; // already registered
|
|
400
|
-
|
|
401
|
-
// Build and cache the serialized descriptor
|
|
402
|
-
const descriptor = buildFileDescriptorProto(schema, fname);
|
|
403
|
-
this._files.set(fname, { schema, descriptor });
|
|
404
|
-
|
|
405
|
-
// Index symbols
|
|
406
|
-
const prefix = schema.package ? schema.package + '.' : '';
|
|
407
|
-
|
|
408
|
-
for (const name of Object.keys(schema.services))
|
|
409
|
-
{
|
|
410
|
-
const fqn = prefix + name;
|
|
411
|
-
this._symbols.set(fqn, fname);
|
|
412
|
-
this._serviceNames.push(fqn);
|
|
413
|
-
|
|
414
|
-
// Index methods
|
|
415
|
-
for (const methodName of Object.keys(schema.services[name].methods))
|
|
416
|
-
{
|
|
417
|
-
this._symbols.set(fqn + '.' + methodName, fname);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
for (const name of Object.keys(schema.messages))
|
|
422
|
-
{
|
|
423
|
-
if (!name.includes('.')) // only top-level
|
|
424
|
-
this._symbols.set(prefix + name, fname);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
for (const name of Object.keys(schema.enums))
|
|
428
|
-
{
|
|
429
|
-
if (!name.includes('.'))
|
|
430
|
-
this._symbols.set(prefix + name, fname);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
log.info('registered schema for reflection: %s (%d bytes)', fname, descriptor.length);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Get the schema for server registration.
|
|
438
|
-
* @returns {object}
|
|
439
|
-
*/
|
|
440
|
-
getSchema()
|
|
441
|
-
{
|
|
442
|
-
return {
|
|
443
|
-
package: 'grpc.reflection.v1',
|
|
444
|
-
services: {
|
|
445
|
-
ServerReflection: {
|
|
446
|
-
methods: {
|
|
447
|
-
ServerReflectionInfo: {
|
|
448
|
-
name: 'ServerReflectionInfo',
|
|
449
|
-
inputType: 'ServerReflectionRequest',
|
|
450
|
-
outputType: 'ServerReflectionResponse',
|
|
451
|
-
clientStreaming: true,
|
|
452
|
-
serverStreaming: true,
|
|
453
|
-
},
|
|
454
|
-
},
|
|
455
|
-
},
|
|
456
|
-
},
|
|
457
|
-
messages: _reflectionMessages,
|
|
458
|
-
enums: {},
|
|
459
|
-
};
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
* Get the handler map.
|
|
464
|
-
* @returns {Object<string, Function>}
|
|
465
|
-
*/
|
|
466
|
-
getHandlers()
|
|
467
|
-
{
|
|
468
|
-
return {
|
|
469
|
-
ServerReflectionInfo: (call) => this._handleReflection(call),
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* Handle the bidirectional reflection stream.
|
|
475
|
-
* @private
|
|
476
|
-
* @param {import('./call').BidiStreamCall} call
|
|
477
|
-
*/
|
|
478
|
-
async _handleReflection(call)
|
|
479
|
-
{
|
|
480
|
-
for await (const req of call)
|
|
481
|
-
{
|
|
482
|
-
if (call._ended || call._cancelled) break;
|
|
483
|
-
|
|
484
|
-
let response;
|
|
485
|
-
|
|
486
|
-
if (req.list_services !== undefined)
|
|
487
|
-
{
|
|
488
|
-
response = this._listServices(req);
|
|
489
|
-
}
|
|
490
|
-
else if (req.file_by_filename)
|
|
491
|
-
{
|
|
492
|
-
response = this._fileByFilename(req);
|
|
493
|
-
}
|
|
494
|
-
else if (req.file_containing_symbol)
|
|
495
|
-
{
|
|
496
|
-
response = this._fileContainingSymbol(req);
|
|
497
|
-
}
|
|
498
|
-
else
|
|
499
|
-
{
|
|
500
|
-
response = {
|
|
501
|
-
valid_host: req.host || '',
|
|
502
|
-
error_response: {
|
|
503
|
-
error_code: GrpcStatus.UNIMPLEMENTED,
|
|
504
|
-
error_message: 'Unsupported reflection request type',
|
|
505
|
-
},
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
call.write(response);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
if (!call._ended) call.end();
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
/**
|
|
516
|
-
* Handle list_services request.
|
|
517
|
-
* @private
|
|
518
|
-
*/
|
|
519
|
-
_listServices(req)
|
|
520
|
-
{
|
|
521
|
-
const services = this._serviceNames.map(name => ({ name }));
|
|
522
|
-
|
|
523
|
-
// Always include the reflection service itself
|
|
524
|
-
if (!services.find(s => s.name === 'grpc.reflection.v1.ServerReflection'))
|
|
525
|
-
services.push({ name: 'grpc.reflection.v1.ServerReflection' });
|
|
526
|
-
|
|
527
|
-
return {
|
|
528
|
-
valid_host: req.host || '',
|
|
529
|
-
list_services_response: { service: services },
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
/**
|
|
534
|
-
* Handle file_by_filename request.
|
|
535
|
-
* @private
|
|
536
|
-
*/
|
|
537
|
-
_fileByFilename(req)
|
|
538
|
-
{
|
|
539
|
-
const entry = this._files.get(req.file_by_filename);
|
|
540
|
-
if (!entry)
|
|
541
|
-
{
|
|
542
|
-
return {
|
|
543
|
-
valid_host: req.host || '',
|
|
544
|
-
error_response: {
|
|
545
|
-
error_code: GrpcStatus.NOT_FOUND,
|
|
546
|
-
error_message: `File not found: ${req.file_by_filename}`,
|
|
547
|
-
},
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
return {
|
|
552
|
-
valid_host: req.host || '',
|
|
553
|
-
file_descriptor_response: {
|
|
554
|
-
file_descriptor_proto: [entry.descriptor],
|
|
555
|
-
},
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
/**
|
|
560
|
-
* Handle file_containing_symbol request.
|
|
561
|
-
* @private
|
|
562
|
-
*/
|
|
563
|
-
_fileContainingSymbol(req)
|
|
564
|
-
{
|
|
565
|
-
const filename = this._symbols.get(req.file_containing_symbol);
|
|
566
|
-
if (!filename)
|
|
567
|
-
{
|
|
568
|
-
return {
|
|
569
|
-
valid_host: req.host || '',
|
|
570
|
-
error_response: {
|
|
571
|
-
error_code: GrpcStatus.NOT_FOUND,
|
|
572
|
-
error_message: `Symbol not found: ${req.file_containing_symbol}`,
|
|
573
|
-
},
|
|
574
|
-
};
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
const entry = this._files.get(filename);
|
|
578
|
-
return {
|
|
579
|
-
valid_host: req.host || '',
|
|
580
|
-
file_descriptor_response: {
|
|
581
|
-
file_descriptor_proto: [entry.descriptor],
|
|
582
|
-
},
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
module.exports = {
|
|
588
|
-
ReflectionService,
|
|
589
|
-
buildFileDescriptorProto,
|
|
590
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* @module grpc/reflection
|
|
3
|
+
* @description gRPC Server Reflection Protocol implementation (grpc.reflection.v1).
|
|
4
|
+
* Enables `grpcurl`, `grpcui`, Postman, and other debugging tools to
|
|
5
|
+
* introspect registered services without supplying `.proto` files.
|
|
6
|
+
*
|
|
7
|
+
* @see https://github.com/grpc/grpc/blob/master/doc/server-reflection.md
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const app = createApp();
|
|
11
|
+
* app.grpc(schema, 'Greeter', handlers);
|
|
12
|
+
* app.grpcReflection(); // dev-only by default
|
|
13
|
+
* app.listen(50051, { http2: true });
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const log = require('../debug')('zero:grpc:reflection');
|
|
17
|
+
const { GrpcStatus } = require('./status');
|
|
18
|
+
const { encode, decode } = require('./codec');
|
|
19
|
+
const { Writer } = require('./codec');
|
|
20
|
+
|
|
21
|
+
// -- Protobuf field type numbers (google.protobuf.FieldDescriptorProto.Type) --
|
|
22
|
+
|
|
23
|
+
const PB_TYPE = {
|
|
24
|
+
double: 1, float: 2, int64: 3, uint64: 4, int32: 5,
|
|
25
|
+
fixed64: 6, fixed32: 7, bool: 8, string: 9, group: 10,
|
|
26
|
+
message: 11, bytes: 12, uint32: 13, enum: 14, sfixed32: 15,
|
|
27
|
+
sfixed64: 16, sint32: 17, sint64: 18,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const PB_LABEL = { optional: 1, required: 2, repeated: 3 };
|
|
31
|
+
|
|
32
|
+
// -- FileDescriptorProto Builder (protobuf self-description) ---
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build a serialized FileDescriptorProto from a parsed proto schema.
|
|
36
|
+
* This is the binary format returned by the reflection service.
|
|
37
|
+
*
|
|
38
|
+
* We hand-construct the protobuf wire format directly since we can't
|
|
39
|
+
* use the codec (which needs descriptors that we're building).
|
|
40
|
+
* Uses the Writer class for encoding.
|
|
41
|
+
*
|
|
42
|
+
* @private
|
|
43
|
+
* @param {object} schema - Parsed proto schema.
|
|
44
|
+
* @param {string} [filename] - File name for the descriptor.
|
|
45
|
+
* @returns {Buffer} Serialized FileDescriptorProto.
|
|
46
|
+
*/
|
|
47
|
+
function buildFileDescriptorProto(schema, filename)
|
|
48
|
+
{
|
|
49
|
+
const w = new Writer();
|
|
50
|
+
|
|
51
|
+
// field 1: name (string)
|
|
52
|
+
if (filename) w.string(1, filename);
|
|
53
|
+
|
|
54
|
+
// field 2: package (string)
|
|
55
|
+
if (schema.package) w.string(2, schema.package);
|
|
56
|
+
|
|
57
|
+
// field 3: dependency (repeated string) — imported file names
|
|
58
|
+
if (schema.imports)
|
|
59
|
+
{
|
|
60
|
+
for (const imp of schema.imports)
|
|
61
|
+
w.string(3, imp.path);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// field 4: message_type (repeated DescriptorProto)
|
|
65
|
+
for (const [name, msg] of Object.entries(schema.messages))
|
|
66
|
+
{
|
|
67
|
+
// Only emit top-level messages (skip nested ones that were flattened)
|
|
68
|
+
if (name.includes('.') && !name.startsWith(schema.package)) continue;
|
|
69
|
+
// Skip if it's a duplicate from flattening (name has package prefix)
|
|
70
|
+
const shortName = name.includes('.') ? name.split('.').pop() : name;
|
|
71
|
+
if (shortName !== name && schema.messages[shortName] === msg) continue;
|
|
72
|
+
|
|
73
|
+
const msgBytes = _buildDescriptorProto(msg, schema);
|
|
74
|
+
w.bytes(4, msgBytes);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// field 5: enum_type (repeated EnumDescriptorProto)
|
|
78
|
+
for (const [name, enumDef] of Object.entries(schema.enums))
|
|
79
|
+
{
|
|
80
|
+
if (name.includes('.')) continue; // skip flattened duplicates
|
|
81
|
+
const enumBytes = _buildEnumDescriptorProto(enumDef);
|
|
82
|
+
w.bytes(5, enumBytes);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// field 6: service (repeated ServiceDescriptorProto)
|
|
86
|
+
for (const [name, svc] of Object.entries(schema.services))
|
|
87
|
+
{
|
|
88
|
+
const svcBytes = _buildServiceDescriptorProto(svc, schema);
|
|
89
|
+
w.bytes(6, svcBytes);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// field 12: syntax (string)
|
|
93
|
+
if (schema.syntax) w.string(12, schema.syntax);
|
|
94
|
+
|
|
95
|
+
return w.finish();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build a DescriptorProto (message descriptor).
|
|
100
|
+
* @private
|
|
101
|
+
*/
|
|
102
|
+
function _buildDescriptorProto(msg, schema)
|
|
103
|
+
{
|
|
104
|
+
const w = new Writer();
|
|
105
|
+
|
|
106
|
+
// field 1: name
|
|
107
|
+
w.string(1, msg.name);
|
|
108
|
+
|
|
109
|
+
// field 2: field (repeated FieldDescriptorProto)
|
|
110
|
+
if (msg.fields)
|
|
111
|
+
{
|
|
112
|
+
for (const field of msg.fields)
|
|
113
|
+
{
|
|
114
|
+
if (field.map)
|
|
115
|
+
{
|
|
116
|
+
const fieldBytes = _buildMapFieldDescriptor(field, schema);
|
|
117
|
+
w.bytes(2, fieldBytes);
|
|
118
|
+
}
|
|
119
|
+
else
|
|
120
|
+
{
|
|
121
|
+
const fieldBytes = _buildFieldDescriptorProto(field, schema);
|
|
122
|
+
w.bytes(2, fieldBytes);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// field 3: nested_type (repeated DescriptorProto)
|
|
128
|
+
if (msg.nested)
|
|
129
|
+
{
|
|
130
|
+
for (const nested of Object.values(msg.nested))
|
|
131
|
+
{
|
|
132
|
+
w.bytes(3, _buildDescriptorProto(nested, schema));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// field 4: enum_type (repeated EnumDescriptorProto)
|
|
137
|
+
if (msg.nestedEnums)
|
|
138
|
+
{
|
|
139
|
+
for (const nested of Object.values(msg.nestedEnums))
|
|
140
|
+
{
|
|
141
|
+
w.bytes(4, _buildEnumDescriptorProto(nested));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// field 8: oneof_decl (repeated OneofDescriptorProto)
|
|
146
|
+
if (msg.oneofs)
|
|
147
|
+
{
|
|
148
|
+
let idx = 0;
|
|
149
|
+
const oneofIndices = {};
|
|
150
|
+
for (const oneofName of Object.keys(msg.oneofs))
|
|
151
|
+
{
|
|
152
|
+
const ow = new Writer();
|
|
153
|
+
ow.string(1, oneofName);
|
|
154
|
+
w.bytes(8, ow.finish());
|
|
155
|
+
oneofIndices[oneofName] = idx++;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return w.finish();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Build a FieldDescriptorProto.
|
|
164
|
+
* @private
|
|
165
|
+
*/
|
|
166
|
+
function _buildFieldDescriptorProto(field, schema)
|
|
167
|
+
{
|
|
168
|
+
const w = new Writer();
|
|
169
|
+
|
|
170
|
+
// field 1: name
|
|
171
|
+
w.string(1, field.name);
|
|
172
|
+
|
|
173
|
+
// field 3: number
|
|
174
|
+
w.int32(3, field.number);
|
|
175
|
+
|
|
176
|
+
// field 4: label
|
|
177
|
+
if (field.repeated) w.int32(4, PB_LABEL.repeated);
|
|
178
|
+
else w.int32(4, PB_LABEL.optional);
|
|
179
|
+
|
|
180
|
+
// field 5: type
|
|
181
|
+
const pbType = _resolveFieldType(field, schema);
|
|
182
|
+
w.int32(5, pbType);
|
|
183
|
+
|
|
184
|
+
// field 6: type_name (for message and enum types)
|
|
185
|
+
if (pbType === PB_TYPE.message || pbType === PB_TYPE.enum)
|
|
186
|
+
{
|
|
187
|
+
const typeName = field.type.startsWith('.') ? field.type : '.' + (schema.package ? schema.package + '.' : '') + field.type;
|
|
188
|
+
w.string(6, typeName);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// field 9: oneof_index (if in a oneof)
|
|
192
|
+
if (field.oneofName !== undefined)
|
|
193
|
+
{
|
|
194
|
+
// Will use the index from the parent — simplified to 0 for now
|
|
195
|
+
w.int32(9, 0);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return w.finish();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Build a map field descriptor (as a repeated message with map_entry option).
|
|
203
|
+
* @private
|
|
204
|
+
*/
|
|
205
|
+
function _buildMapFieldDescriptor(field, schema)
|
|
206
|
+
{
|
|
207
|
+
const w = new Writer();
|
|
208
|
+
w.string(1, field.name);
|
|
209
|
+
w.int32(3, field.number);
|
|
210
|
+
w.int32(4, PB_LABEL.repeated);
|
|
211
|
+
w.int32(5, PB_TYPE.message);
|
|
212
|
+
// type_name for the auto-generated entry type
|
|
213
|
+
const entryName = field.name.charAt(0).toUpperCase() + field.name.slice(1) + 'Entry';
|
|
214
|
+
w.string(6, '.' + (schema.package ? schema.package + '.' : '') + entryName);
|
|
215
|
+
return w.finish();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Build an EnumDescriptorProto.
|
|
220
|
+
* @private
|
|
221
|
+
*/
|
|
222
|
+
function _buildEnumDescriptorProto(enumDef)
|
|
223
|
+
{
|
|
224
|
+
const w = new Writer();
|
|
225
|
+
|
|
226
|
+
// field 1: name
|
|
227
|
+
w.string(1, enumDef.name);
|
|
228
|
+
|
|
229
|
+
// field 2: value (repeated EnumValueDescriptorProto)
|
|
230
|
+
if (enumDef.values)
|
|
231
|
+
{
|
|
232
|
+
for (const [name, number] of Object.entries(enumDef.values))
|
|
233
|
+
{
|
|
234
|
+
const vw = new Writer();
|
|
235
|
+
vw.string(1, name);
|
|
236
|
+
vw.int32(2, number);
|
|
237
|
+
w.bytes(2, vw.finish());
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return w.finish();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Build a ServiceDescriptorProto.
|
|
246
|
+
* @private
|
|
247
|
+
*/
|
|
248
|
+
function _buildServiceDescriptorProto(svc, schema)
|
|
249
|
+
{
|
|
250
|
+
const w = new Writer();
|
|
251
|
+
|
|
252
|
+
// field 1: name
|
|
253
|
+
w.string(1, svc.name);
|
|
254
|
+
|
|
255
|
+
// field 2: method (repeated MethodDescriptorProto)
|
|
256
|
+
for (const method of Object.values(svc.methods))
|
|
257
|
+
{
|
|
258
|
+
const mw = new Writer();
|
|
259
|
+
mw.string(1, method.name);
|
|
260
|
+
// input type (fully qualified)
|
|
261
|
+
mw.string(2, '.' + (schema.package ? schema.package + '.' : '') + method.inputType);
|
|
262
|
+
// output type
|
|
263
|
+
mw.string(3, '.' + (schema.package ? schema.package + '.' : '') + method.outputType);
|
|
264
|
+
// client_streaming
|
|
265
|
+
if (method.clientStreaming) mw.bool(5, true);
|
|
266
|
+
// server_streaming
|
|
267
|
+
if (method.serverStreaming) mw.bool(6, true);
|
|
268
|
+
|
|
269
|
+
w.bytes(2, mw.finish());
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return w.finish();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Resolve a field type string to protobuf type number.
|
|
277
|
+
* @private
|
|
278
|
+
*/
|
|
279
|
+
function _resolveFieldType(field, schema)
|
|
280
|
+
{
|
|
281
|
+
if (PB_TYPE[field.type]) return PB_TYPE[field.type];
|
|
282
|
+
if (schema.enums && schema.enums[field.type]) return PB_TYPE.enum;
|
|
283
|
+
if (schema.messages && schema.messages[field.type]) return PB_TYPE.message;
|
|
284
|
+
// Default to message type for unknown types (custom messages)
|
|
285
|
+
return PB_TYPE.message;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// -- Reflection Request/Response Descriptors ----------------
|
|
289
|
+
|
|
290
|
+
const _reflectionRequestDesc = {
|
|
291
|
+
name: 'ServerReflectionRequest',
|
|
292
|
+
fields: [
|
|
293
|
+
{ name: 'host', type: 'string', number: 1, repeated: false, optional: false, map: false },
|
|
294
|
+
{ name: 'file_by_filename', type: 'string', number: 3, repeated: false, optional: false, map: false, oneofName: 'message_request' },
|
|
295
|
+
{ name: 'file_containing_symbol', type: 'string', number: 4, repeated: false, optional: false, map: false, oneofName: 'message_request' },
|
|
296
|
+
{ name: 'list_services', type: 'string', number: 7, repeated: false, optional: false, map: false, oneofName: 'message_request' },
|
|
297
|
+
],
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const _reflectionResponseDesc = {
|
|
301
|
+
name: 'ServerReflectionResponse',
|
|
302
|
+
fields: [
|
|
303
|
+
{ name: 'valid_host', type: 'string', number: 1, repeated: false, optional: false, map: false },
|
|
304
|
+
{ name: 'original_request', type: 'ServerReflectionRequest', number: 2, repeated: false, optional: false, map: false },
|
|
305
|
+
{ name: 'file_descriptor_response', type: 'FileDescriptorResponse', number: 4, repeated: false, optional: false, map: false, oneofName: 'message_response' },
|
|
306
|
+
{ name: 'all_extension_numbers_response', type: 'ExtensionNumberResponse', number: 5, repeated: false, optional: false, map: false, oneofName: 'message_response' },
|
|
307
|
+
{ name: 'list_services_response', type: 'ListServiceResponse', number: 6, repeated: false, optional: false, map: false, oneofName: 'message_response' },
|
|
308
|
+
{ name: 'error_response', type: 'ErrorResponse', number: 7, repeated: false, optional: false, map: false, oneofName: 'message_response' },
|
|
309
|
+
],
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const _fileDescriptorResponseDesc = {
|
|
313
|
+
name: 'FileDescriptorResponse',
|
|
314
|
+
fields: [
|
|
315
|
+
{ name: 'file_descriptor_proto', type: 'bytes', number: 1, repeated: true, optional: false, map: false },
|
|
316
|
+
],
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const _listServiceResponseDesc = {
|
|
320
|
+
name: 'ListServiceResponse',
|
|
321
|
+
fields: [
|
|
322
|
+
{ name: 'service', type: 'ServiceResponse', number: 1, repeated: true, optional: false, map: false },
|
|
323
|
+
],
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const _serviceResponseDesc = {
|
|
327
|
+
name: 'ServiceResponse',
|
|
328
|
+
fields: [
|
|
329
|
+
{ name: 'name', type: 'string', number: 1, repeated: false, optional: false, map: false },
|
|
330
|
+
],
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const _errorResponseDesc = {
|
|
334
|
+
name: 'ErrorResponse',
|
|
335
|
+
fields: [
|
|
336
|
+
{ name: 'error_code', type: 'int32', number: 1, repeated: false, optional: false, map: false },
|
|
337
|
+
{ name: 'error_message', type: 'string', number: 2, repeated: false, optional: false, map: false },
|
|
338
|
+
],
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const _reflectionMessages = {
|
|
342
|
+
ServerReflectionRequest: _reflectionRequestDesc,
|
|
343
|
+
ServerReflectionResponse: _reflectionResponseDesc,
|
|
344
|
+
FileDescriptorResponse: _fileDescriptorResponseDesc,
|
|
345
|
+
ListServiceResponse: _listServiceResponseDesc,
|
|
346
|
+
ServiceResponse: _serviceResponseDesc,
|
|
347
|
+
ErrorResponse: _errorResponseDesc,
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// -- Reflection Service ----------------------------------------
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Server Reflection service implementation.
|
|
354
|
+
* Caches serialized descriptors at registration time.
|
|
355
|
+
*
|
|
356
|
+
* @class
|
|
357
|
+
*/
|
|
358
|
+
class ReflectionService
|
|
359
|
+
{
|
|
360
|
+
/**
|
|
361
|
+
* @param {object} [opts]
|
|
362
|
+
* @param {boolean} [opts.production=false] - Enable in production.
|
|
363
|
+
*/
|
|
364
|
+
constructor(opts = {})
|
|
365
|
+
{
|
|
366
|
+
/** @private */
|
|
367
|
+
this._production = opts.production || false;
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Registered schemas indexed by filename.
|
|
371
|
+
* @type {Map<string, { schema: object, descriptor: Buffer }>}
|
|
372
|
+
*/
|
|
373
|
+
this._files = new Map();
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Symbol → filename mapping for file_containing_symbol lookups.
|
|
377
|
+
* @type {Map<string, string>}
|
|
378
|
+
*/
|
|
379
|
+
this._symbols = new Map();
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* All registered service names (fully qualified).
|
|
383
|
+
* @type {string[]}
|
|
384
|
+
*/
|
|
385
|
+
this._serviceNames = [];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Register a schema for reflection.
|
|
390
|
+
* Builds and caches the FileDescriptorProto at registration time.
|
|
391
|
+
*
|
|
392
|
+
* @param {object} schema - Parsed proto schema.
|
|
393
|
+
* @param {string} [filename] - Filename to use. Defaults to schema package or '<inline>'.
|
|
394
|
+
*/
|
|
395
|
+
addSchema(schema, filename)
|
|
396
|
+
{
|
|
397
|
+
const fname = filename || (schema.package ? schema.package.replace(/\./g, '/') + '.proto' : '<inline>');
|
|
398
|
+
|
|
399
|
+
if (this._files.has(fname)) return; // already registered
|
|
400
|
+
|
|
401
|
+
// Build and cache the serialized descriptor
|
|
402
|
+
const descriptor = buildFileDescriptorProto(schema, fname);
|
|
403
|
+
this._files.set(fname, { schema, descriptor });
|
|
404
|
+
|
|
405
|
+
// Index symbols
|
|
406
|
+
const prefix = schema.package ? schema.package + '.' : '';
|
|
407
|
+
|
|
408
|
+
for (const name of Object.keys(schema.services))
|
|
409
|
+
{
|
|
410
|
+
const fqn = prefix + name;
|
|
411
|
+
this._symbols.set(fqn, fname);
|
|
412
|
+
this._serviceNames.push(fqn);
|
|
413
|
+
|
|
414
|
+
// Index methods
|
|
415
|
+
for (const methodName of Object.keys(schema.services[name].methods))
|
|
416
|
+
{
|
|
417
|
+
this._symbols.set(fqn + '.' + methodName, fname);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
for (const name of Object.keys(schema.messages))
|
|
422
|
+
{
|
|
423
|
+
if (!name.includes('.')) // only top-level
|
|
424
|
+
this._symbols.set(prefix + name, fname);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
for (const name of Object.keys(schema.enums))
|
|
428
|
+
{
|
|
429
|
+
if (!name.includes('.'))
|
|
430
|
+
this._symbols.set(prefix + name, fname);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
log.info('registered schema for reflection: %s (%d bytes)', fname, descriptor.length);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Get the schema for server registration.
|
|
438
|
+
* @returns {object}
|
|
439
|
+
*/
|
|
440
|
+
getSchema()
|
|
441
|
+
{
|
|
442
|
+
return {
|
|
443
|
+
package: 'grpc.reflection.v1',
|
|
444
|
+
services: {
|
|
445
|
+
ServerReflection: {
|
|
446
|
+
methods: {
|
|
447
|
+
ServerReflectionInfo: {
|
|
448
|
+
name: 'ServerReflectionInfo',
|
|
449
|
+
inputType: 'ServerReflectionRequest',
|
|
450
|
+
outputType: 'ServerReflectionResponse',
|
|
451
|
+
clientStreaming: true,
|
|
452
|
+
serverStreaming: true,
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
messages: _reflectionMessages,
|
|
458
|
+
enums: {},
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Get the handler map.
|
|
464
|
+
* @returns {Object<string, Function>}
|
|
465
|
+
*/
|
|
466
|
+
getHandlers()
|
|
467
|
+
{
|
|
468
|
+
return {
|
|
469
|
+
ServerReflectionInfo: (call) => this._handleReflection(call),
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Handle the bidirectional reflection stream.
|
|
475
|
+
* @private
|
|
476
|
+
* @param {import('./call').BidiStreamCall} call
|
|
477
|
+
*/
|
|
478
|
+
async _handleReflection(call)
|
|
479
|
+
{
|
|
480
|
+
for await (const req of call)
|
|
481
|
+
{
|
|
482
|
+
if (call._ended || call._cancelled) break;
|
|
483
|
+
|
|
484
|
+
let response;
|
|
485
|
+
|
|
486
|
+
if (req.list_services !== undefined)
|
|
487
|
+
{
|
|
488
|
+
response = this._listServices(req);
|
|
489
|
+
}
|
|
490
|
+
else if (req.file_by_filename)
|
|
491
|
+
{
|
|
492
|
+
response = this._fileByFilename(req);
|
|
493
|
+
}
|
|
494
|
+
else if (req.file_containing_symbol)
|
|
495
|
+
{
|
|
496
|
+
response = this._fileContainingSymbol(req);
|
|
497
|
+
}
|
|
498
|
+
else
|
|
499
|
+
{
|
|
500
|
+
response = {
|
|
501
|
+
valid_host: req.host || '',
|
|
502
|
+
error_response: {
|
|
503
|
+
error_code: GrpcStatus.UNIMPLEMENTED,
|
|
504
|
+
error_message: 'Unsupported reflection request type',
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
call.write(response);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (!call._ended) call.end();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Handle list_services request.
|
|
517
|
+
* @private
|
|
518
|
+
*/
|
|
519
|
+
_listServices(req)
|
|
520
|
+
{
|
|
521
|
+
const services = this._serviceNames.map(name => ({ name }));
|
|
522
|
+
|
|
523
|
+
// Always include the reflection service itself
|
|
524
|
+
if (!services.find(s => s.name === 'grpc.reflection.v1.ServerReflection'))
|
|
525
|
+
services.push({ name: 'grpc.reflection.v1.ServerReflection' });
|
|
526
|
+
|
|
527
|
+
return {
|
|
528
|
+
valid_host: req.host || '',
|
|
529
|
+
list_services_response: { service: services },
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Handle file_by_filename request.
|
|
535
|
+
* @private
|
|
536
|
+
*/
|
|
537
|
+
_fileByFilename(req)
|
|
538
|
+
{
|
|
539
|
+
const entry = this._files.get(req.file_by_filename);
|
|
540
|
+
if (!entry)
|
|
541
|
+
{
|
|
542
|
+
return {
|
|
543
|
+
valid_host: req.host || '',
|
|
544
|
+
error_response: {
|
|
545
|
+
error_code: GrpcStatus.NOT_FOUND,
|
|
546
|
+
error_message: `File not found: ${req.file_by_filename}`,
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return {
|
|
552
|
+
valid_host: req.host || '',
|
|
553
|
+
file_descriptor_response: {
|
|
554
|
+
file_descriptor_proto: [entry.descriptor],
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Handle file_containing_symbol request.
|
|
561
|
+
* @private
|
|
562
|
+
*/
|
|
563
|
+
_fileContainingSymbol(req)
|
|
564
|
+
{
|
|
565
|
+
const filename = this._symbols.get(req.file_containing_symbol);
|
|
566
|
+
if (!filename)
|
|
567
|
+
{
|
|
568
|
+
return {
|
|
569
|
+
valid_host: req.host || '',
|
|
570
|
+
error_response: {
|
|
571
|
+
error_code: GrpcStatus.NOT_FOUND,
|
|
572
|
+
error_message: `Symbol not found: ${req.file_containing_symbol}`,
|
|
573
|
+
},
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const entry = this._files.get(filename);
|
|
578
|
+
return {
|
|
579
|
+
valid_host: req.host || '',
|
|
580
|
+
file_descriptor_response: {
|
|
581
|
+
file_descriptor_proto: [entry.descriptor],
|
|
582
|
+
},
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
module.exports = {
|
|
588
|
+
ReflectionService,
|
|
589
|
+
buildFileDescriptorProto,
|
|
590
|
+
};
|