@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.
Files changed (128) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +460 -443
  3. package/index.js +414 -412
  4. package/lib/app.js +1172 -1172
  5. package/lib/auth/authorize.js +399 -399
  6. package/lib/auth/enrollment.js +367 -367
  7. package/lib/auth/index.js +57 -57
  8. package/lib/auth/jwt.js +731 -731
  9. package/lib/auth/oauth.js +362 -362
  10. package/lib/auth/session.js +588 -588
  11. package/lib/auth/trustedDevice.js +409 -409
  12. package/lib/auth/twoFactor.js +1150 -1150
  13. package/lib/auth/webauthn.js +946 -946
  14. package/lib/body/index.js +14 -14
  15. package/lib/body/json.js +109 -109
  16. package/lib/body/multipart.js +440 -440
  17. package/lib/body/raw.js +71 -71
  18. package/lib/body/rawBuffer.js +160 -160
  19. package/lib/body/sendError.js +25 -25
  20. package/lib/body/text.js +75 -75
  21. package/lib/body/typeMatch.js +41 -41
  22. package/lib/body/urlencoded.js +235 -235
  23. package/lib/cli.js +845 -845
  24. package/lib/cluster.js +666 -666
  25. package/lib/debug.js +372 -372
  26. package/lib/env/index.js +465 -465
  27. package/lib/errors.js +683 -683
  28. package/lib/fetch/index.js +256 -256
  29. package/lib/grpc/balancer.js +378 -378
  30. package/lib/grpc/call.js +708 -708
  31. package/lib/grpc/client.js +764 -764
  32. package/lib/grpc/codec.js +1221 -1221
  33. package/lib/grpc/credentials.js +398 -398
  34. package/lib/grpc/frame.js +262 -262
  35. package/lib/grpc/health.js +287 -287
  36. package/lib/grpc/index.js +121 -121
  37. package/lib/grpc/metadata.js +461 -461
  38. package/lib/grpc/proto.js +821 -821
  39. package/lib/grpc/reflection.js +590 -590
  40. package/lib/grpc/server.js +445 -445
  41. package/lib/grpc/status.js +118 -118
  42. package/lib/grpc/watch.js +173 -173
  43. package/lib/http/index.js +10 -10
  44. package/lib/http/request.js +727 -727
  45. package/lib/http/response.js +799 -799
  46. package/lib/lifecycle.js +557 -557
  47. package/lib/middleware/compress.js +230 -230
  48. package/lib/middleware/cookieParser.js +237 -237
  49. package/lib/middleware/cors.js +93 -93
  50. package/lib/middleware/csrf.js +137 -137
  51. package/lib/middleware/errorHandler.js +101 -101
  52. package/lib/middleware/helmet.js +175 -175
  53. package/lib/middleware/index.js +19 -17
  54. package/lib/middleware/logger.js +74 -74
  55. package/lib/middleware/rateLimit.js +88 -88
  56. package/lib/middleware/requestId.js +53 -53
  57. package/lib/middleware/static.js +326 -326
  58. package/lib/middleware/timeout.js +71 -71
  59. package/lib/middleware/validator.js +255 -255
  60. package/lib/observe/health.js +326 -326
  61. package/lib/observe/index.js +50 -50
  62. package/lib/observe/logger.js +359 -359
  63. package/lib/observe/metrics.js +805 -805
  64. package/lib/observe/tracing.js +592 -592
  65. package/lib/orm/adapters/json.js +290 -290
  66. package/lib/orm/adapters/memory.js +764 -764
  67. package/lib/orm/adapters/mongo.js +764 -764
  68. package/lib/orm/adapters/mysql.js +933 -933
  69. package/lib/orm/adapters/postgres.js +1144 -1144
  70. package/lib/orm/adapters/redis.js +1534 -1534
  71. package/lib/orm/adapters/sql-base.js +212 -212
  72. package/lib/orm/adapters/sqlite.js +858 -858
  73. package/lib/orm/audit.js +649 -649
  74. package/lib/orm/cache.js +394 -394
  75. package/lib/orm/geo.js +387 -387
  76. package/lib/orm/index.js +784 -784
  77. package/lib/orm/migrate.js +432 -432
  78. package/lib/orm/model.js +1706 -1706
  79. package/lib/orm/plugin.js +375 -375
  80. package/lib/orm/procedures.js +836 -836
  81. package/lib/orm/profiler.js +233 -233
  82. package/lib/orm/query.js +1772 -1772
  83. package/lib/orm/replicas.js +241 -241
  84. package/lib/orm/schema.js +307 -307
  85. package/lib/orm/search.js +380 -380
  86. package/lib/orm/seed/data/commerce.js +136 -136
  87. package/lib/orm/seed/data/internet.js +111 -111
  88. package/lib/orm/seed/data/locations.js +204 -204
  89. package/lib/orm/seed/data/names.js +338 -338
  90. package/lib/orm/seed/data/person.js +128 -128
  91. package/lib/orm/seed/data/phone.js +211 -211
  92. package/lib/orm/seed/data/words.js +134 -134
  93. package/lib/orm/seed/factory.js +178 -178
  94. package/lib/orm/seed/fake.js +1186 -1186
  95. package/lib/orm/seed/index.js +18 -18
  96. package/lib/orm/seed/rng.js +70 -70
  97. package/lib/orm/seed/seeder.js +124 -124
  98. package/lib/orm/seed/unique.js +68 -68
  99. package/lib/orm/snapshot.js +366 -366
  100. package/lib/orm/tenancy.js +605 -605
  101. package/lib/orm/views.js +350 -350
  102. package/lib/router/index.js +436 -436
  103. package/lib/sse/index.js +8 -8
  104. package/lib/sse/stream.js +349 -349
  105. package/lib/ws/connection.js +451 -451
  106. package/lib/ws/handshake.js +125 -125
  107. package/lib/ws/index.js +14 -14
  108. package/lib/ws/room.js +223 -223
  109. package/package.json +73 -73
  110. package/types/app.d.ts +223 -223
  111. package/types/auth.d.ts +520 -520
  112. package/types/body.d.ts +14 -0
  113. package/types/cli.d.ts +2 -0
  114. package/types/cluster.d.ts +75 -75
  115. package/types/env.d.ts +80 -80
  116. package/types/errors.d.ts +316 -316
  117. package/types/fetch.d.ts +43 -43
  118. package/types/grpc.d.ts +432 -432
  119. package/types/index.d.ts +384 -384
  120. package/types/lifecycle.d.ts +60 -60
  121. package/types/middleware.d.ts +320 -320
  122. package/types/observe.d.ts +304 -304
  123. package/types/orm.d.ts +1887 -1887
  124. package/types/request.d.ts +109 -109
  125. package/types/response.d.ts +157 -157
  126. package/types/router.d.ts +78 -78
  127. package/types/sse.d.ts +78 -78
  128. package/types/websocket.d.ts +126 -126
package/lib/grpc/proto.js CHANGED
@@ -1,821 +1,821 @@
1
- /**
2
- * @module grpc/proto
3
- * @description Zero-dependency proto3 parser — reads `.proto` file text and produces
4
- * message descriptors, enum definitions, and service/RPC declarations
5
- * that the codec and server use at runtime.
6
- *
7
- * Supports:
8
- * - `syntax = "proto3";`
9
- * - `package`, `option`, `import` (recorded but not resolved)
10
- * - Scalar types, enums, nested messages, `oneof`, `map`, `repeated`
11
- * - Services with unary, server-streaming, client-streaming, and bidi RPCs
12
- * - Comments (// and /* ... *​/)
13
- * - Reserved fields and field options like `[deprecated = true]`
14
- *
15
- * @see https://protobuf.dev/programming-guides/proto3/
16
- *
17
- * @example
18
- * const { parseProto } = require('./proto');
19
- * const schema = parseProto(fs.readFileSync('chat.proto', 'utf8'));
20
- * // schema.messages — { MessageName: { fields: [...] } }
21
- * // schema.enums — { EnumName: { values: { ... } } }
22
- * // schema.services — { ServiceName: { methods: { ... } } }
23
- */
24
-
25
- const fs = require('fs');
26
- const path = require('path');
27
- const log = require('../debug')('zero:grpc');
28
-
29
- // -- Token Types -------------------------------------------
30
-
31
- /** @private */
32
- const TOK = {
33
- IDENT: 'IDENT',
34
- NUMBER: 'NUMBER',
35
- STRING: 'STRING',
36
- SYMBOL: 'SYMBOL',
37
- EOF: 'EOF',
38
- };
39
-
40
- // -- Lexer -------------------------------------------------
41
-
42
- /**
43
- * Tokenize proto3 source text.
44
- * @private
45
- * @param {string} source
46
- * @returns {{ type: string, value: string, line: number }[]}
47
- */
48
- function tokenize(source)
49
- {
50
- const tokens = [];
51
- let i = 0;
52
- let line = 1;
53
-
54
- while (i < source.length)
55
- {
56
- const ch = source[i];
57
-
58
- // Newlines
59
- if (ch === '\n') { line++; i++; continue; }
60
- if (ch === '\r') { i++; continue; }
61
-
62
- // Whitespace
63
- if (ch === ' ' || ch === '\t') { i++; continue; }
64
-
65
- // Single-line comment
66
- if (ch === '/' && source[i + 1] === '/')
67
- {
68
- while (i < source.length && source[i] !== '\n') i++;
69
- continue;
70
- }
71
-
72
- // Block comment
73
- if (ch === '/' && source[i + 1] === '*')
74
- {
75
- i += 2;
76
- while (i < source.length - 1)
77
- {
78
- if (source[i] === '\n') line++;
79
- if (source[i] === '*' && source[i + 1] === '/')
80
- {
81
- i += 2;
82
- break;
83
- }
84
- i++;
85
- }
86
- continue;
87
- }
88
-
89
- // String literal
90
- if (ch === '"' || ch === "'")
91
- {
92
- const quote = ch;
93
- let str = '';
94
- i++;
95
- while (i < source.length && source[i] !== quote)
96
- {
97
- if (source[i] === '\\' && i + 1 < source.length)
98
- {
99
- const esc = source[i + 1];
100
- if (esc === 'n') str += '\n';
101
- else if (esc === 't') str += '\t';
102
- else if (esc === '\\') str += '\\';
103
- else if (esc === quote) str += quote;
104
- else str += esc;
105
- i += 2;
106
- }
107
- else
108
- {
109
- str += source[i++];
110
- }
111
- }
112
- i++; // skip closing quote
113
- tokens.push({ type: TOK.STRING, value: str, line });
114
- continue;
115
- }
116
-
117
- // Number (integer or float, including negative)
118
- if ((ch >= '0' && ch <= '9') || (ch === '-' && source[i + 1] >= '0' && source[i + 1] <= '9'))
119
- {
120
- let num = '';
121
- if (ch === '-') { num = '-'; i++; }
122
-
123
- // Hex
124
- if (source[i] === '0' && (source[i + 1] === 'x' || source[i + 1] === 'X'))
125
- {
126
- num += '0x'; i += 2;
127
- while (i < source.length && /[0-9a-fA-F]/.test(source[i])) num += source[i++];
128
- }
129
- else
130
- {
131
- while (i < source.length && ((source[i] >= '0' && source[i] <= '9') || source[i] === '.' || source[i] === 'e' || source[i] === 'E' || source[i] === '+' || source[i] === '-'))
132
- {
133
- // Avoid consuming the next field's minus sign
134
- if ((source[i] === '+' || source[i] === '-') && source[i - 1] !== 'e' && source[i - 1] !== 'E') break;
135
- num += source[i++];
136
- }
137
- }
138
- tokens.push({ type: TOK.NUMBER, value: num, line });
139
- continue;
140
- }
141
-
142
- // Identifier or keyword
143
- if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_')
144
- {
145
- let ident = '';
146
- while (i < source.length && ((source[i] >= 'a' && source[i] <= 'z') || (source[i] >= 'A' && source[i] <= 'Z') || (source[i] >= '0' && source[i] <= '9') || source[i] === '_' || source[i] === '.'))
147
- {
148
- ident += source[i++];
149
- }
150
- tokens.push({ type: TOK.IDENT, value: ident, line });
151
- continue;
152
- }
153
-
154
- // Symbols: { } ( ) ; = , < > [ ]
155
- tokens.push({ type: TOK.SYMBOL, value: ch, line });
156
- i++;
157
- }
158
-
159
- tokens.push({ type: TOK.EOF, value: '', line });
160
- return tokens;
161
- }
162
-
163
- // -- Parser ------------------------------------------------
164
-
165
- /**
166
- * Parse a proto3 source string into a structured schema.
167
- *
168
- * @param {string} source - Proto3 source text.
169
- * @param {object} [opts] - Parser options.
170
- * @param {string} [opts.filename] - File name for error messages.
171
- * @param {string} [opts.basePath] - Base directory for resolving imports.
172
- * @param {boolean} [opts.resolveImports=false] - Whether to recursively parse imported files.
173
- * @returns {ProtoSchema} Parsed schema with messages, enums, and services.
174
- *
175
- * @example
176
- * const schema = parseProto('syntax = "proto3"; message Ping { string msg = 1; }');
177
- * schema.messages.Ping.fields[0].name; // 'msg'
178
- * schema.messages.Ping.fields[0].type; // 'string'
179
- */
180
- function parseProto(source, opts = {})
181
- {
182
- const tokens = tokenize(source);
183
- let pos = 0;
184
- const filename = opts.filename || '<inline>';
185
-
186
- const schema = {
187
- syntax: 'proto3',
188
- package: '',
189
- imports: [],
190
- options: {},
191
- messages: {},
192
- enums: {},
193
- services: {},
194
- };
195
-
196
- /**
197
- * Get current token.
198
- * @private
199
- */
200
- function peek()
201
- {
202
- return tokens[pos];
203
- }
204
-
205
- /**
206
- * Consume and return the current token.
207
- * @private
208
- */
209
- function next()
210
- {
211
- return tokens[pos++];
212
- }
213
-
214
- /**
215
- * Expect a specific token value or type.
216
- * @private
217
- */
218
- function expect(value, type)
219
- {
220
- const tok = next();
221
- if (type && tok.type !== type)
222
- throw new SyntaxError(`${filename}:${tok.line}: expected ${type} "${value}", got ${tok.type} "${tok.value}"`);
223
- if (value && tok.value !== value)
224
- throw new SyntaxError(`${filename}:${tok.line}: expected "${value}", got "${tok.value}"`);
225
- return tok;
226
- }
227
-
228
- /**
229
- * Check if current token matches value.
230
- * @private
231
- */
232
- function match(value)
233
- {
234
- return peek().value === value;
235
- }
236
-
237
- /**
238
- * Consume if current token matches value.
239
- * @private
240
- */
241
- function eat(value)
242
- {
243
- if (match(value)) { next(); return true; }
244
- return false;
245
- }
246
-
247
- // -- Top-Level Parsing ---------------------------------
248
-
249
- while (peek().type !== TOK.EOF)
250
- {
251
- const tok = peek();
252
-
253
- if (tok.value === 'syntax')
254
- {
255
- next(); expect('='); schema.syntax = next().value; expect(';');
256
- if (schema.syntax !== 'proto3')
257
- log.warn('proto file uses syntax "%s" — only proto3 is fully supported', schema.syntax);
258
- }
259
- else if (tok.value === 'package')
260
- {
261
- next(); schema.package = next().value; expect(';');
262
- }
263
- else if (tok.value === 'import')
264
- {
265
- next();
266
- const weak = eat('weak');
267
- const pub = eat('public');
268
- const importPath = next().value;
269
- expect(';');
270
- schema.imports.push({ path: importPath, weak, public: pub });
271
-
272
- if (opts.resolveImports && opts.basePath)
273
- {
274
- try
275
- {
276
- const fullPath = path.resolve(opts.basePath, importPath);
277
- const importSource = fs.readFileSync(fullPath, 'utf8');
278
- const importSchema = parseProto(importSource, {
279
- filename: importPath,
280
- basePath: path.dirname(fullPath),
281
- resolveImports: true,
282
- });
283
- // Merge imported definitions
284
- Object.assign(schema.messages, importSchema.messages);
285
- Object.assign(schema.enums, importSchema.enums);
286
- Object.assign(schema.services, importSchema.services);
287
- }
288
- catch (err)
289
- {
290
- log.warn('failed to resolve import "%s": %s', importPath, err.message);
291
- }
292
- }
293
- }
294
- else if (tok.value === 'option')
295
- {
296
- next(); _parseOption(schema.options);
297
- }
298
- else if (tok.value === 'message')
299
- {
300
- next();
301
- const name = next().value;
302
- schema.messages[name] = _parseMessage(name);
303
- }
304
- else if (tok.value === 'enum')
305
- {
306
- next();
307
- const name = next().value;
308
- schema.enums[name] = _parseEnum(name);
309
- }
310
- else if (tok.value === 'service')
311
- {
312
- next();
313
- const name = next().value;
314
- schema.services[name] = _parseService(name);
315
- }
316
- else if (tok.value === ';')
317
- {
318
- next(); // skip stray semicolons
319
- }
320
- else
321
- {
322
- throw new SyntaxError(`${filename}:${tok.line}: unexpected token "${tok.value}"`);
323
- }
324
- }
325
-
326
- // Link enum definitions into message fields
327
- _linkEnums(schema);
328
-
329
- log.info('parsed %s: %d messages, %d enums, %d services',
330
- filename,
331
- Object.keys(schema.messages).length,
332
- Object.keys(schema.enums).length,
333
- Object.keys(schema.services).length,
334
- );
335
-
336
- return schema;
337
-
338
- // -- Helper Functions (closures over peek/next/expect/match/eat) --
339
-
340
- /**
341
- * Parse a message definition (including nested messages and enums).
342
- * @param {string} msgName
343
- * @returns {object}
344
- */
345
- function _parseMessage(msgName)
346
- {
347
- const msg = { name: msgName, fields: [], oneofs: {}, nested: {}, nestedEnums: {}, options: {} };
348
-
349
- expect('{');
350
-
351
- while (!match('}'))
352
- {
353
- const tok = peek();
354
-
355
- if (tok.value === 'message')
356
- {
357
- next();
358
- const nestedName = next().value;
359
- msg.nested[nestedName] = _parseMessage(nestedName);
360
- }
361
- else if (tok.value === 'enum')
362
- {
363
- next();
364
- const enumName = next().value;
365
- msg.nestedEnums[enumName] = _parseEnum(enumName);
366
- }
367
- else if (tok.value === 'oneof')
368
- {
369
- next();
370
- const oneofName = next().value;
371
- msg.oneofs[oneofName] = _parseOneof(oneofName, msg.fields);
372
- }
373
- else if (tok.value === 'map')
374
- {
375
- _parseMapField(msg.fields);
376
- }
377
- else if (tok.value === 'reserved')
378
- {
379
- _parseReserved();
380
- }
381
- else if (tok.value === 'option')
382
- {
383
- next(); _parseOption(msg.options);
384
- }
385
- else if (tok.value === ';')
386
- {
387
- next();
388
- }
389
- else
390
- {
391
- _parseField(msg.fields);
392
- }
393
- }
394
-
395
- expect('}');
396
- if (peek().value === ';') next();
397
-
398
- return msg;
399
- }
400
-
401
- /**
402
- * Parse a field declaration.
403
- * @param {object[]} fieldsArray
404
- */
405
- function _parseField(fieldsArray)
406
- {
407
- let repeated = false;
408
- let optional = false;
409
-
410
- if (match('repeated')) { next(); repeated = true; }
411
- else if (match('optional')) { next(); optional = true; }
412
-
413
- const type = next().value;
414
- const fName = next().value;
415
- expect('=');
416
- const number = parseInt(next().value, 10);
417
-
418
- const fieldOpts = {};
419
- if (match('['))
420
- {
421
- next();
422
- while (!match(']'))
423
- {
424
- const optName = next().value;
425
- expect('=');
426
- const optVal = next().value;
427
- fieldOpts[optName] = optVal;
428
- eat(',');
429
- }
430
- expect(']');
431
- }
432
-
433
- expect(';');
434
-
435
- fieldsArray.push({
436
- name: fName,
437
- type,
438
- number,
439
- repeated,
440
- optional,
441
- map: false,
442
- options: fieldOpts,
443
- });
444
- }
445
-
446
- /**
447
- * Parse a map field: `map<KeyType, ValueType> name = N;`
448
- * @param {object[]} fieldsArray
449
- */
450
- function _parseMapField(fieldsArray)
451
- {
452
- expect('map');
453
- expect('<');
454
- const keyType = next().value;
455
- expect(',');
456
- const valueType = next().value;
457
- expect('>');
458
- const fName = next().value;
459
- expect('=');
460
- const number = parseInt(next().value, 10);
461
-
462
- const fieldOpts = {};
463
- if (match('['))
464
- {
465
- next();
466
- while (!match(']'))
467
- {
468
- const optName = next().value;
469
- expect('=');
470
- const optVal = next().value;
471
- fieldOpts[optName] = optVal;
472
- eat(',');
473
- }
474
- expect(']');
475
- }
476
-
477
- expect(';');
478
-
479
- fieldsArray.push({
480
- name: fName,
481
- type: `map<${keyType},${valueType}>`,
482
- keyType,
483
- valueType,
484
- number,
485
- repeated: false,
486
- optional: false,
487
- map: true,
488
- mapKeyType: keyType,
489
- mapValueType: valueType,
490
- options: fieldOpts,
491
- });
492
- }
493
-
494
- /**
495
- * Parse oneof block.
496
- * @param {string} oneofName
497
- * @param {object[]} fieldsArray
498
- * @returns {string[]}
499
- */
500
- function _parseOneof(oneofName, fieldsArray)
501
- {
502
- const fieldNames = [];
503
- expect('{');
504
-
505
- while (!match('}'))
506
- {
507
- if (match('option'))
508
- {
509
- next(); _parseOption({});
510
- }
511
- else if (match(';'))
512
- {
513
- next();
514
- }
515
- else
516
- {
517
- const type = next().value;
518
- const fName = next().value;
519
- expect('=');
520
- const number = parseInt(next().value, 10);
521
-
522
- const fieldOpts = {};
523
- if (match('['))
524
- {
525
- next();
526
- while (!match(']'))
527
- {
528
- const optName = next().value;
529
- expect('=');
530
- const optVal = next().value;
531
- fieldOpts[optName] = optVal;
532
- eat(',');
533
- }
534
- expect(']');
535
- }
536
-
537
- expect(';');
538
-
539
- fieldsArray.push({
540
- name: fName,
541
- type,
542
- number,
543
- repeated: false,
544
- optional: false,
545
- map: false,
546
- oneofName,
547
- options: fieldOpts,
548
- });
549
- fieldNames.push(fName);
550
- }
551
- }
552
-
553
- expect('}');
554
- return fieldNames;
555
- }
556
-
557
- /**
558
- * Parse an enum definition.
559
- * @param {string} eName
560
- * @returns {object}
561
- */
562
- function _parseEnum(eName)
563
- {
564
- const enumDef = { name: eName, values: {}, options: {} };
565
- expect('{');
566
-
567
- while (!match('}'))
568
- {
569
- if (match('option'))
570
- {
571
- next(); _parseOption(enumDef.options);
572
- }
573
- else if (match('reserved'))
574
- {
575
- _parseReserved();
576
- }
577
- else if (match(';'))
578
- {
579
- next();
580
- }
581
- else
582
- {
583
- const valueName = next().value;
584
- expect('=');
585
- const valueNumber = parseInt(next().value, 10);
586
-
587
- if (match('['))
588
- {
589
- next();
590
- while (!match(']'))
591
- {
592
- next(); eat('='); next(); eat(',');
593
- }
594
- expect(']');
595
- }
596
-
597
- expect(';');
598
- enumDef.values[valueName] = valueNumber;
599
- }
600
- }
601
-
602
- expect('}');
603
- if (peek().value === ';') next();
604
-
605
- return enumDef;
606
- }
607
-
608
- /**
609
- * Parse a service definition with RPC methods.
610
- * @param {string} sName
611
- * @returns {object}
612
- */
613
- function _parseService(sName)
614
- {
615
- const service = { name: sName, methods: {}, options: {} };
616
- expect('{');
617
-
618
- while (!match('}'))
619
- {
620
- if (match('rpc'))
621
- {
622
- next();
623
- const methodName = next().value;
624
- expect('(');
625
- const clientStreaming = eat('stream');
626
- const inputType = next().value;
627
- expect(')');
628
- expect('returns');
629
- expect('(');
630
- const serverStreaming = eat('stream');
631
- const outputType = next().value;
632
- expect(')');
633
-
634
- const methodOpts = {};
635
- if (match('{'))
636
- {
637
- next();
638
- while (!match('}'))
639
- {
640
- if (match('option'))
641
- {
642
- next(); _parseOption(methodOpts);
643
- }
644
- else
645
- {
646
- next();
647
- }
648
- }
649
- expect('}');
650
- }
651
- else
652
- {
653
- expect(';');
654
- }
655
-
656
- service.methods[methodName] = {
657
- name: methodName,
658
- inputType,
659
- outputType,
660
- clientStreaming: !!clientStreaming,
661
- serverStreaming: !!serverStreaming,
662
- options: methodOpts,
663
- };
664
- }
665
- else if (match('option'))
666
- {
667
- next(); _parseOption(service.options);
668
- }
669
- else if (match(';'))
670
- {
671
- next();
672
- }
673
- else
674
- {
675
- next();
676
- }
677
- }
678
-
679
- expect('}');
680
- if (peek().value === ';') next();
681
-
682
- return service;
683
- }
684
-
685
- /**
686
- * Parse an option statement: `option name = value;`
687
- * @param {object} target
688
- */
689
- function _parseOption(target)
690
- {
691
- let optName = '';
692
- if (match('('))
693
- {
694
- next();
695
- optName = '(' + next().value + ')';
696
- expect(')');
697
- }
698
- else
699
- {
700
- optName = next().value;
701
- }
702
-
703
- while (match('.'))
704
- {
705
- next();
706
- optName += '.' + next().value;
707
- }
708
-
709
- expect('=');
710
- const value = next().value;
711
- expect(';');
712
- target[optName] = value;
713
- }
714
-
715
- /**
716
- * Skip a `reserved` statement.
717
- */
718
- function _parseReserved()
719
- {
720
- next(); // consume 'reserved'
721
- while (peek().value !== ';' && peek().type !== TOK.EOF) next();
722
- if (peek().value === ';') next();
723
- }
724
- }
725
-
726
- // -- Post-Processing --------------------------------------
727
-
728
- /**
729
- * Link enum definitions into message fields so the codec knows how to encode/decode them.
730
- * Also flattens nested messages and enums into the top-level maps for easy lookup.
731
- * @private
732
- * @param {object} schema
733
- */
734
- function _linkEnums(schema)
735
- {
736
- // Flatten nested messages and enums
737
- const flatMsgs = {};
738
- const flatEnums = {};
739
-
740
- function flatten(messages, enums, prefix)
741
- {
742
- for (const [name, msg] of Object.entries(messages))
743
- {
744
- const fullName = prefix ? `${prefix}.${name}` : name;
745
- flatMsgs[fullName] = msg;
746
- flatMsgs[name] = msg; // also store short name for convenience
747
-
748
- if (msg.nested) flatten(msg.nested, {}, fullName);
749
- if (msg.nestedEnums)
750
- {
751
- for (const [eName, eDef] of Object.entries(msg.nestedEnums))
752
- {
753
- const fullEnum = prefix ? `${prefix}.${name}.${eName}` : `${name}.${eName}`;
754
- flatEnums[fullEnum] = eDef;
755
- flatEnums[eName] = eDef; // short name
756
- }
757
- }
758
- }
759
-
760
- for (const [name, def] of Object.entries(enums))
761
- {
762
- const fullName = prefix ? `${prefix}.${name}` : name;
763
- flatEnums[fullName] = def;
764
- flatEnums[name] = def;
765
- }
766
- }
767
-
768
- flatten(schema.messages, schema.enums, schema.package);
769
-
770
- // Merge flattened into schema
771
- Object.assign(schema.messages, flatMsgs);
772
- Object.assign(schema.enums, flatEnums);
773
-
774
- // Link enum types into message fields
775
- for (const msg of Object.values(schema.messages))
776
- {
777
- if (!msg.fields) continue;
778
- for (const field of msg.fields)
779
- {
780
- if (!field.map && schema.enums[field.type])
781
- {
782
- field.enumDef = schema.enums[field.type];
783
- }
784
- // Map value enums
785
- if (field.map && schema.enums[field.valueType])
786
- {
787
- field.enumDef = schema.enums[field.valueType];
788
- }
789
- }
790
- }
791
- }
792
-
793
- // -- File Loader -------------------------------------------
794
-
795
- /**
796
- * Parse a `.proto` file from disk.
797
- *
798
- * @param {string} filePath - Path to the `.proto` file.
799
- * @param {object} [opts] - Parser options.
800
- * @param {boolean} [opts.resolveImports=false] - Whether to recursively resolve imports.
801
- * @returns {ProtoSchema} Parsed schema.
802
- *
803
- * @example
804
- * const schema = parseProtoFile('./protos/chat.proto');
805
- */
806
- function parseProtoFile(filePath, opts = {})
807
- {
808
- const resolved = path.resolve(filePath);
809
- const source = fs.readFileSync(resolved, 'utf8');
810
- return parseProto(source, {
811
- filename: path.basename(resolved),
812
- basePath: path.dirname(resolved),
813
- ...opts,
814
- });
815
- }
816
-
817
- module.exports = {
818
- parseProto,
819
- parseProtoFile,
820
- tokenize,
821
- };
1
+ /**
2
+ * @module grpc/proto
3
+ * @description Zero-dependency proto3 parser — reads `.proto` file text and produces
4
+ * message descriptors, enum definitions, and service/RPC declarations
5
+ * that the codec and server use at runtime.
6
+ *
7
+ * Supports:
8
+ * - `syntax = "proto3";`
9
+ * - `package`, `option`, `import` (recorded but not resolved)
10
+ * - Scalar types, enums, nested messages, `oneof`, `map`, `repeated`
11
+ * - Services with unary, server-streaming, client-streaming, and bidi RPCs
12
+ * - Comments (// and /* ... *​/)
13
+ * - Reserved fields and field options like `[deprecated = true]`
14
+ *
15
+ * @see https://protobuf.dev/programming-guides/proto3/
16
+ *
17
+ * @example
18
+ * const { parseProto } = require('./proto');
19
+ * const schema = parseProto(fs.readFileSync('chat.proto', 'utf8'));
20
+ * // schema.messages — { MessageName: { fields: [...] } }
21
+ * // schema.enums — { EnumName: { values: { ... } } }
22
+ * // schema.services — { ServiceName: { methods: { ... } } }
23
+ */
24
+
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+ const log = require('../debug')('zero:grpc');
28
+
29
+ // -- Token Types -------------------------------------------
30
+
31
+ /** @private */
32
+ const TOK = {
33
+ IDENT: 'IDENT',
34
+ NUMBER: 'NUMBER',
35
+ STRING: 'STRING',
36
+ SYMBOL: 'SYMBOL',
37
+ EOF: 'EOF',
38
+ };
39
+
40
+ // -- Lexer -------------------------------------------------
41
+
42
+ /**
43
+ * Tokenize proto3 source text.
44
+ * @private
45
+ * @param {string} source
46
+ * @returns {{ type: string, value: string, line: number }[]}
47
+ */
48
+ function tokenize(source)
49
+ {
50
+ const tokens = [];
51
+ let i = 0;
52
+ let line = 1;
53
+
54
+ while (i < source.length)
55
+ {
56
+ const ch = source[i];
57
+
58
+ // Newlines
59
+ if (ch === '\n') { line++; i++; continue; }
60
+ if (ch === '\r') { i++; continue; }
61
+
62
+ // Whitespace
63
+ if (ch === ' ' || ch === '\t') { i++; continue; }
64
+
65
+ // Single-line comment
66
+ if (ch === '/' && source[i + 1] === '/')
67
+ {
68
+ while (i < source.length && source[i] !== '\n') i++;
69
+ continue;
70
+ }
71
+
72
+ // Block comment
73
+ if (ch === '/' && source[i + 1] === '*')
74
+ {
75
+ i += 2;
76
+ while (i < source.length - 1)
77
+ {
78
+ if (source[i] === '\n') line++;
79
+ if (source[i] === '*' && source[i + 1] === '/')
80
+ {
81
+ i += 2;
82
+ break;
83
+ }
84
+ i++;
85
+ }
86
+ continue;
87
+ }
88
+
89
+ // String literal
90
+ if (ch === '"' || ch === "'")
91
+ {
92
+ const quote = ch;
93
+ let str = '';
94
+ i++;
95
+ while (i < source.length && source[i] !== quote)
96
+ {
97
+ if (source[i] === '\\' && i + 1 < source.length)
98
+ {
99
+ const esc = source[i + 1];
100
+ if (esc === 'n') str += '\n';
101
+ else if (esc === 't') str += '\t';
102
+ else if (esc === '\\') str += '\\';
103
+ else if (esc === quote) str += quote;
104
+ else str += esc;
105
+ i += 2;
106
+ }
107
+ else
108
+ {
109
+ str += source[i++];
110
+ }
111
+ }
112
+ i++; // skip closing quote
113
+ tokens.push({ type: TOK.STRING, value: str, line });
114
+ continue;
115
+ }
116
+
117
+ // Number (integer or float, including negative)
118
+ if ((ch >= '0' && ch <= '9') || (ch === '-' && source[i + 1] >= '0' && source[i + 1] <= '9'))
119
+ {
120
+ let num = '';
121
+ if (ch === '-') { num = '-'; i++; }
122
+
123
+ // Hex
124
+ if (source[i] === '0' && (source[i + 1] === 'x' || source[i + 1] === 'X'))
125
+ {
126
+ num += '0x'; i += 2;
127
+ while (i < source.length && /[0-9a-fA-F]/.test(source[i])) num += source[i++];
128
+ }
129
+ else
130
+ {
131
+ while (i < source.length && ((source[i] >= '0' && source[i] <= '9') || source[i] === '.' || source[i] === 'e' || source[i] === 'E' || source[i] === '+' || source[i] === '-'))
132
+ {
133
+ // Avoid consuming the next field's minus sign
134
+ if ((source[i] === '+' || source[i] === '-') && source[i - 1] !== 'e' && source[i - 1] !== 'E') break;
135
+ num += source[i++];
136
+ }
137
+ }
138
+ tokens.push({ type: TOK.NUMBER, value: num, line });
139
+ continue;
140
+ }
141
+
142
+ // Identifier or keyword
143
+ if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_')
144
+ {
145
+ let ident = '';
146
+ while (i < source.length && ((source[i] >= 'a' && source[i] <= 'z') || (source[i] >= 'A' && source[i] <= 'Z') || (source[i] >= '0' && source[i] <= '9') || source[i] === '_' || source[i] === '.'))
147
+ {
148
+ ident += source[i++];
149
+ }
150
+ tokens.push({ type: TOK.IDENT, value: ident, line });
151
+ continue;
152
+ }
153
+
154
+ // Symbols: { } ( ) ; = , < > [ ]
155
+ tokens.push({ type: TOK.SYMBOL, value: ch, line });
156
+ i++;
157
+ }
158
+
159
+ tokens.push({ type: TOK.EOF, value: '', line });
160
+ return tokens;
161
+ }
162
+
163
+ // -- Parser ------------------------------------------------
164
+
165
+ /**
166
+ * Parse a proto3 source string into a structured schema.
167
+ *
168
+ * @param {string} source - Proto3 source text.
169
+ * @param {object} [opts] - Parser options.
170
+ * @param {string} [opts.filename] - File name for error messages.
171
+ * @param {string} [opts.basePath] - Base directory for resolving imports.
172
+ * @param {boolean} [opts.resolveImports=false] - Whether to recursively parse imported files.
173
+ * @returns {ProtoSchema} Parsed schema with messages, enums, and services.
174
+ *
175
+ * @example
176
+ * const schema = parseProto('syntax = "proto3"; message Ping { string msg = 1; }');
177
+ * schema.messages.Ping.fields[0].name; // 'msg'
178
+ * schema.messages.Ping.fields[0].type; // 'string'
179
+ */
180
+ function parseProto(source, opts = {})
181
+ {
182
+ const tokens = tokenize(source);
183
+ let pos = 0;
184
+ const filename = opts.filename || '<inline>';
185
+
186
+ const schema = {
187
+ syntax: 'proto3',
188
+ package: '',
189
+ imports: [],
190
+ options: {},
191
+ messages: {},
192
+ enums: {},
193
+ services: {},
194
+ };
195
+
196
+ /**
197
+ * Get current token.
198
+ * @private
199
+ */
200
+ function peek()
201
+ {
202
+ return tokens[pos];
203
+ }
204
+
205
+ /**
206
+ * Consume and return the current token.
207
+ * @private
208
+ */
209
+ function next()
210
+ {
211
+ return tokens[pos++];
212
+ }
213
+
214
+ /**
215
+ * Expect a specific token value or type.
216
+ * @private
217
+ */
218
+ function expect(value, type)
219
+ {
220
+ const tok = next();
221
+ if (type && tok.type !== type)
222
+ throw new SyntaxError(`${filename}:${tok.line}: expected ${type} "${value}", got ${tok.type} "${tok.value}"`);
223
+ if (value && tok.value !== value)
224
+ throw new SyntaxError(`${filename}:${tok.line}: expected "${value}", got "${tok.value}"`);
225
+ return tok;
226
+ }
227
+
228
+ /**
229
+ * Check if current token matches value.
230
+ * @private
231
+ */
232
+ function match(value)
233
+ {
234
+ return peek().value === value;
235
+ }
236
+
237
+ /**
238
+ * Consume if current token matches value.
239
+ * @private
240
+ */
241
+ function eat(value)
242
+ {
243
+ if (match(value)) { next(); return true; }
244
+ return false;
245
+ }
246
+
247
+ // -- Top-Level Parsing ---------------------------------
248
+
249
+ while (peek().type !== TOK.EOF)
250
+ {
251
+ const tok = peek();
252
+
253
+ if (tok.value === 'syntax')
254
+ {
255
+ next(); expect('='); schema.syntax = next().value; expect(';');
256
+ if (schema.syntax !== 'proto3')
257
+ log.warn('proto file uses syntax "%s" — only proto3 is fully supported', schema.syntax);
258
+ }
259
+ else if (tok.value === 'package')
260
+ {
261
+ next(); schema.package = next().value; expect(';');
262
+ }
263
+ else if (tok.value === 'import')
264
+ {
265
+ next();
266
+ const weak = eat('weak');
267
+ const pub = eat('public');
268
+ const importPath = next().value;
269
+ expect(';');
270
+ schema.imports.push({ path: importPath, weak, public: pub });
271
+
272
+ if (opts.resolveImports && opts.basePath)
273
+ {
274
+ try
275
+ {
276
+ const fullPath = path.resolve(opts.basePath, importPath);
277
+ const importSource = fs.readFileSync(fullPath, 'utf8');
278
+ const importSchema = parseProto(importSource, {
279
+ filename: importPath,
280
+ basePath: path.dirname(fullPath),
281
+ resolveImports: true,
282
+ });
283
+ // Merge imported definitions
284
+ Object.assign(schema.messages, importSchema.messages);
285
+ Object.assign(schema.enums, importSchema.enums);
286
+ Object.assign(schema.services, importSchema.services);
287
+ }
288
+ catch (err)
289
+ {
290
+ log.warn('failed to resolve import "%s": %s', importPath, err.message);
291
+ }
292
+ }
293
+ }
294
+ else if (tok.value === 'option')
295
+ {
296
+ next(); _parseOption(schema.options);
297
+ }
298
+ else if (tok.value === 'message')
299
+ {
300
+ next();
301
+ const name = next().value;
302
+ schema.messages[name] = _parseMessage(name);
303
+ }
304
+ else if (tok.value === 'enum')
305
+ {
306
+ next();
307
+ const name = next().value;
308
+ schema.enums[name] = _parseEnum(name);
309
+ }
310
+ else if (tok.value === 'service')
311
+ {
312
+ next();
313
+ const name = next().value;
314
+ schema.services[name] = _parseService(name);
315
+ }
316
+ else if (tok.value === ';')
317
+ {
318
+ next(); // skip stray semicolons
319
+ }
320
+ else
321
+ {
322
+ throw new SyntaxError(`${filename}:${tok.line}: unexpected token "${tok.value}"`);
323
+ }
324
+ }
325
+
326
+ // Link enum definitions into message fields
327
+ _linkEnums(schema);
328
+
329
+ log.info('parsed %s: %d messages, %d enums, %d services',
330
+ filename,
331
+ Object.keys(schema.messages).length,
332
+ Object.keys(schema.enums).length,
333
+ Object.keys(schema.services).length,
334
+ );
335
+
336
+ return schema;
337
+
338
+ // -- Helper Functions (closures over peek/next/expect/match/eat) --
339
+
340
+ /**
341
+ * Parse a message definition (including nested messages and enums).
342
+ * @param {string} msgName
343
+ * @returns {object}
344
+ */
345
+ function _parseMessage(msgName)
346
+ {
347
+ const msg = { name: msgName, fields: [], oneofs: {}, nested: {}, nestedEnums: {}, options: {} };
348
+
349
+ expect('{');
350
+
351
+ while (!match('}'))
352
+ {
353
+ const tok = peek();
354
+
355
+ if (tok.value === 'message')
356
+ {
357
+ next();
358
+ const nestedName = next().value;
359
+ msg.nested[nestedName] = _parseMessage(nestedName);
360
+ }
361
+ else if (tok.value === 'enum')
362
+ {
363
+ next();
364
+ const enumName = next().value;
365
+ msg.nestedEnums[enumName] = _parseEnum(enumName);
366
+ }
367
+ else if (tok.value === 'oneof')
368
+ {
369
+ next();
370
+ const oneofName = next().value;
371
+ msg.oneofs[oneofName] = _parseOneof(oneofName, msg.fields);
372
+ }
373
+ else if (tok.value === 'map')
374
+ {
375
+ _parseMapField(msg.fields);
376
+ }
377
+ else if (tok.value === 'reserved')
378
+ {
379
+ _parseReserved();
380
+ }
381
+ else if (tok.value === 'option')
382
+ {
383
+ next(); _parseOption(msg.options);
384
+ }
385
+ else if (tok.value === ';')
386
+ {
387
+ next();
388
+ }
389
+ else
390
+ {
391
+ _parseField(msg.fields);
392
+ }
393
+ }
394
+
395
+ expect('}');
396
+ if (peek().value === ';') next();
397
+
398
+ return msg;
399
+ }
400
+
401
+ /**
402
+ * Parse a field declaration.
403
+ * @param {object[]} fieldsArray
404
+ */
405
+ function _parseField(fieldsArray)
406
+ {
407
+ let repeated = false;
408
+ let optional = false;
409
+
410
+ if (match('repeated')) { next(); repeated = true; }
411
+ else if (match('optional')) { next(); optional = true; }
412
+
413
+ const type = next().value;
414
+ const fName = next().value;
415
+ expect('=');
416
+ const number = parseInt(next().value, 10);
417
+
418
+ const fieldOpts = {};
419
+ if (match('['))
420
+ {
421
+ next();
422
+ while (!match(']'))
423
+ {
424
+ const optName = next().value;
425
+ expect('=');
426
+ const optVal = next().value;
427
+ fieldOpts[optName] = optVal;
428
+ eat(',');
429
+ }
430
+ expect(']');
431
+ }
432
+
433
+ expect(';');
434
+
435
+ fieldsArray.push({
436
+ name: fName,
437
+ type,
438
+ number,
439
+ repeated,
440
+ optional,
441
+ map: false,
442
+ options: fieldOpts,
443
+ });
444
+ }
445
+
446
+ /**
447
+ * Parse a map field: `map<KeyType, ValueType> name = N;`
448
+ * @param {object[]} fieldsArray
449
+ */
450
+ function _parseMapField(fieldsArray)
451
+ {
452
+ expect('map');
453
+ expect('<');
454
+ const keyType = next().value;
455
+ expect(',');
456
+ const valueType = next().value;
457
+ expect('>');
458
+ const fName = next().value;
459
+ expect('=');
460
+ const number = parseInt(next().value, 10);
461
+
462
+ const fieldOpts = {};
463
+ if (match('['))
464
+ {
465
+ next();
466
+ while (!match(']'))
467
+ {
468
+ const optName = next().value;
469
+ expect('=');
470
+ const optVal = next().value;
471
+ fieldOpts[optName] = optVal;
472
+ eat(',');
473
+ }
474
+ expect(']');
475
+ }
476
+
477
+ expect(';');
478
+
479
+ fieldsArray.push({
480
+ name: fName,
481
+ type: `map<${keyType},${valueType}>`,
482
+ keyType,
483
+ valueType,
484
+ number,
485
+ repeated: false,
486
+ optional: false,
487
+ map: true,
488
+ mapKeyType: keyType,
489
+ mapValueType: valueType,
490
+ options: fieldOpts,
491
+ });
492
+ }
493
+
494
+ /**
495
+ * Parse oneof block.
496
+ * @param {string} oneofName
497
+ * @param {object[]} fieldsArray
498
+ * @returns {string[]}
499
+ */
500
+ function _parseOneof(oneofName, fieldsArray)
501
+ {
502
+ const fieldNames = [];
503
+ expect('{');
504
+
505
+ while (!match('}'))
506
+ {
507
+ if (match('option'))
508
+ {
509
+ next(); _parseOption({});
510
+ }
511
+ else if (match(';'))
512
+ {
513
+ next();
514
+ }
515
+ else
516
+ {
517
+ const type = next().value;
518
+ const fName = next().value;
519
+ expect('=');
520
+ const number = parseInt(next().value, 10);
521
+
522
+ const fieldOpts = {};
523
+ if (match('['))
524
+ {
525
+ next();
526
+ while (!match(']'))
527
+ {
528
+ const optName = next().value;
529
+ expect('=');
530
+ const optVal = next().value;
531
+ fieldOpts[optName] = optVal;
532
+ eat(',');
533
+ }
534
+ expect(']');
535
+ }
536
+
537
+ expect(';');
538
+
539
+ fieldsArray.push({
540
+ name: fName,
541
+ type,
542
+ number,
543
+ repeated: false,
544
+ optional: false,
545
+ map: false,
546
+ oneofName,
547
+ options: fieldOpts,
548
+ });
549
+ fieldNames.push(fName);
550
+ }
551
+ }
552
+
553
+ expect('}');
554
+ return fieldNames;
555
+ }
556
+
557
+ /**
558
+ * Parse an enum definition.
559
+ * @param {string} eName
560
+ * @returns {object}
561
+ */
562
+ function _parseEnum(eName)
563
+ {
564
+ const enumDef = { name: eName, values: {}, options: {} };
565
+ expect('{');
566
+
567
+ while (!match('}'))
568
+ {
569
+ if (match('option'))
570
+ {
571
+ next(); _parseOption(enumDef.options);
572
+ }
573
+ else if (match('reserved'))
574
+ {
575
+ _parseReserved();
576
+ }
577
+ else if (match(';'))
578
+ {
579
+ next();
580
+ }
581
+ else
582
+ {
583
+ const valueName = next().value;
584
+ expect('=');
585
+ const valueNumber = parseInt(next().value, 10);
586
+
587
+ if (match('['))
588
+ {
589
+ next();
590
+ while (!match(']'))
591
+ {
592
+ next(); eat('='); next(); eat(',');
593
+ }
594
+ expect(']');
595
+ }
596
+
597
+ expect(';');
598
+ enumDef.values[valueName] = valueNumber;
599
+ }
600
+ }
601
+
602
+ expect('}');
603
+ if (peek().value === ';') next();
604
+
605
+ return enumDef;
606
+ }
607
+
608
+ /**
609
+ * Parse a service definition with RPC methods.
610
+ * @param {string} sName
611
+ * @returns {object}
612
+ */
613
+ function _parseService(sName)
614
+ {
615
+ const service = { name: sName, methods: {}, options: {} };
616
+ expect('{');
617
+
618
+ while (!match('}'))
619
+ {
620
+ if (match('rpc'))
621
+ {
622
+ next();
623
+ const methodName = next().value;
624
+ expect('(');
625
+ const clientStreaming = eat('stream');
626
+ const inputType = next().value;
627
+ expect(')');
628
+ expect('returns');
629
+ expect('(');
630
+ const serverStreaming = eat('stream');
631
+ const outputType = next().value;
632
+ expect(')');
633
+
634
+ const methodOpts = {};
635
+ if (match('{'))
636
+ {
637
+ next();
638
+ while (!match('}'))
639
+ {
640
+ if (match('option'))
641
+ {
642
+ next(); _parseOption(methodOpts);
643
+ }
644
+ else
645
+ {
646
+ next();
647
+ }
648
+ }
649
+ expect('}');
650
+ }
651
+ else
652
+ {
653
+ expect(';');
654
+ }
655
+
656
+ service.methods[methodName] = {
657
+ name: methodName,
658
+ inputType,
659
+ outputType,
660
+ clientStreaming: !!clientStreaming,
661
+ serverStreaming: !!serverStreaming,
662
+ options: methodOpts,
663
+ };
664
+ }
665
+ else if (match('option'))
666
+ {
667
+ next(); _parseOption(service.options);
668
+ }
669
+ else if (match(';'))
670
+ {
671
+ next();
672
+ }
673
+ else
674
+ {
675
+ next();
676
+ }
677
+ }
678
+
679
+ expect('}');
680
+ if (peek().value === ';') next();
681
+
682
+ return service;
683
+ }
684
+
685
+ /**
686
+ * Parse an option statement: `option name = value;`
687
+ * @param {object} target
688
+ */
689
+ function _parseOption(target)
690
+ {
691
+ let optName = '';
692
+ if (match('('))
693
+ {
694
+ next();
695
+ optName = '(' + next().value + ')';
696
+ expect(')');
697
+ }
698
+ else
699
+ {
700
+ optName = next().value;
701
+ }
702
+
703
+ while (match('.'))
704
+ {
705
+ next();
706
+ optName += '.' + next().value;
707
+ }
708
+
709
+ expect('=');
710
+ const value = next().value;
711
+ expect(';');
712
+ target[optName] = value;
713
+ }
714
+
715
+ /**
716
+ * Skip a `reserved` statement.
717
+ */
718
+ function _parseReserved()
719
+ {
720
+ next(); // consume 'reserved'
721
+ while (peek().value !== ';' && peek().type !== TOK.EOF) next();
722
+ if (peek().value === ';') next();
723
+ }
724
+ }
725
+
726
+ // -- Post-Processing --------------------------------------
727
+
728
+ /**
729
+ * Link enum definitions into message fields so the codec knows how to encode/decode them.
730
+ * Also flattens nested messages and enums into the top-level maps for easy lookup.
731
+ * @private
732
+ * @param {object} schema
733
+ */
734
+ function _linkEnums(schema)
735
+ {
736
+ // Flatten nested messages and enums
737
+ const flatMsgs = {};
738
+ const flatEnums = {};
739
+
740
+ function flatten(messages, enums, prefix)
741
+ {
742
+ for (const [name, msg] of Object.entries(messages))
743
+ {
744
+ const fullName = prefix ? `${prefix}.${name}` : name;
745
+ flatMsgs[fullName] = msg;
746
+ flatMsgs[name] = msg; // also store short name for convenience
747
+
748
+ if (msg.nested) flatten(msg.nested, {}, fullName);
749
+ if (msg.nestedEnums)
750
+ {
751
+ for (const [eName, eDef] of Object.entries(msg.nestedEnums))
752
+ {
753
+ const fullEnum = prefix ? `${prefix}.${name}.${eName}` : `${name}.${eName}`;
754
+ flatEnums[fullEnum] = eDef;
755
+ flatEnums[eName] = eDef; // short name
756
+ }
757
+ }
758
+ }
759
+
760
+ for (const [name, def] of Object.entries(enums))
761
+ {
762
+ const fullName = prefix ? `${prefix}.${name}` : name;
763
+ flatEnums[fullName] = def;
764
+ flatEnums[name] = def;
765
+ }
766
+ }
767
+
768
+ flatten(schema.messages, schema.enums, schema.package);
769
+
770
+ // Merge flattened into schema
771
+ Object.assign(schema.messages, flatMsgs);
772
+ Object.assign(schema.enums, flatEnums);
773
+
774
+ // Link enum types into message fields
775
+ for (const msg of Object.values(schema.messages))
776
+ {
777
+ if (!msg.fields) continue;
778
+ for (const field of msg.fields)
779
+ {
780
+ if (!field.map && schema.enums[field.type])
781
+ {
782
+ field.enumDef = schema.enums[field.type];
783
+ }
784
+ // Map value enums
785
+ if (field.map && schema.enums[field.valueType])
786
+ {
787
+ field.enumDef = schema.enums[field.valueType];
788
+ }
789
+ }
790
+ }
791
+ }
792
+
793
+ // -- File Loader -------------------------------------------
794
+
795
+ /**
796
+ * Parse a `.proto` file from disk.
797
+ *
798
+ * @param {string} filePath - Path to the `.proto` file.
799
+ * @param {object} [opts] - Parser options.
800
+ * @param {boolean} [opts.resolveImports=false] - Whether to recursively resolve imports.
801
+ * @returns {ProtoSchema} Parsed schema.
802
+ *
803
+ * @example
804
+ * const schema = parseProtoFile('./protos/chat.proto');
805
+ */
806
+ function parseProtoFile(filePath, opts = {})
807
+ {
808
+ const resolved = path.resolve(filePath);
809
+ const source = fs.readFileSync(resolved, 'utf8');
810
+ return parseProto(source, {
811
+ filename: path.basename(resolved),
812
+ basePath: path.dirname(resolved),
813
+ ...opts,
814
+ });
815
+ }
816
+
817
+ module.exports = {
818
+ parseProto,
819
+ parseProtoFile,
820
+ tokenize,
821
+ };