@zero-server/sdk 0.9.0 → 0.9.2
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 -437
- 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 +460 -460
- 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 +136 -136
- 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 +254 -254
- 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/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/metadata.js
CHANGED
|
@@ -1,461 +1,461 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module grpc/metadata
|
|
3
|
-
* @description gRPC metadata container — typed key-value pairs transmitted as
|
|
4
|
-
* HTTP/2 headers (initial metadata) and trailers (trailing metadata).
|
|
5
|
-
* Keys ending in `-bin` carry binary values (base64-encoded on the wire).
|
|
6
|
-
* All other keys carry ASCII string values.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* const md = new Metadata();
|
|
10
|
-
* md.set('x-request-id', '123');
|
|
11
|
-
* md.add('x-tags', 'alpha');
|
|
12
|
-
* md.add('x-tags', 'beta');
|
|
13
|
-
* md.getAll('x-tags'); // ['alpha', 'beta']
|
|
14
|
-
*
|
|
15
|
-
* @example | Binary metadata
|
|
16
|
-
* md.set('icon-bin', Buffer.from([0x89, 0x50, 0x4e, 0x47]));
|
|
17
|
-
* md.get('icon-bin'); // <Buffer 89 50 4e 47>
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
const log = require('../debug')('zero:grpc');
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Reserved HTTP/2 pseudo-headers that must not be treated as gRPC metadata.
|
|
24
|
-
* @type {Set<string>}
|
|
25
|
-
* @private
|
|
26
|
-
*/
|
|
27
|
-
const RESERVED = new Set([':authority', ':method', ':path', ':scheme', ':status']);
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Headers managed by the gRPC framing layer, not user metadata.
|
|
31
|
-
* @type {Set<string>}
|
|
32
|
-
* @private
|
|
33
|
-
*/
|
|
34
|
-
const GRPC_INTERNAL = new Set([
|
|
35
|
-
'content-type', 'te', 'grpc-timeout', 'grpc-encoding',
|
|
36
|
-
'grpc-accept-encoding', 'grpc-status', 'grpc-message',
|
|
37
|
-
'user-agent', 'host',
|
|
38
|
-
]);
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Maximum metadata key length (prevents abuse).
|
|
42
|
-
* @type {number}
|
|
43
|
-
*/
|
|
44
|
-
const MAX_KEY_LENGTH = 256;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Maximum total metadata size in bytes (soft limit — 8 KB default, configurable).
|
|
48
|
-
* @type {number}
|
|
49
|
-
*/
|
|
50
|
-
const DEFAULT_MAX_METADATA_SIZE = 8192;
|
|
51
|
-
|
|
52
|
-
// -- Metadata Class ----------------------------------------
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* gRPC metadata container — type-safe key-value pairs for headers and trailers.
|
|
56
|
-
*
|
|
57
|
-
* @class
|
|
58
|
-
*
|
|
59
|
-
* @param {object} [opts] - Configuration options.
|
|
60
|
-
* @param {number} [opts.maxSize=8192] - Maximum total serialized metadata size in bytes.
|
|
61
|
-
*
|
|
62
|
-
* @example
|
|
63
|
-
* const md = new Metadata();
|
|
64
|
-
* md.set('authorization', 'Bearer tok123');
|
|
65
|
-
* md.set('x-trace-id', crypto.randomUUID());
|
|
66
|
-
*/
|
|
67
|
-
class Metadata
|
|
68
|
-
{
|
|
69
|
-
constructor(opts = {})
|
|
70
|
-
{
|
|
71
|
-
/** @private */
|
|
72
|
-
this._map = new Map();
|
|
73
|
-
/** @private */
|
|
74
|
-
this._maxSize = opts.maxSize || DEFAULT_MAX_METADATA_SIZE;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// -- Core Operations -----------------------------------
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Set a metadata key to a single value, replacing any existing values.
|
|
81
|
-
* Binary keys (ending in `-bin`) accept Buffer values; all others accept strings.
|
|
82
|
-
*
|
|
83
|
-
* @param {string} key - Metadata key (lowercase, no whitespace).
|
|
84
|
-
* @param {string|Buffer} value - The value to store.
|
|
85
|
-
* @returns {Metadata} `this` for chaining.
|
|
86
|
-
*
|
|
87
|
-
* @example
|
|
88
|
-
* md.set('x-request-id', 'abc-123');
|
|
89
|
-
*/
|
|
90
|
-
set(key, value)
|
|
91
|
-
{
|
|
92
|
-
key = this._validateKey(key);
|
|
93
|
-
this._validateValue(key, value);
|
|
94
|
-
this._map.set(key, [value]);
|
|
95
|
-
return this;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Add a value to a metadata key without replacing existing values.
|
|
100
|
-
* Allows multi-valued metadata (e.g. multiple tags or roles).
|
|
101
|
-
*
|
|
102
|
-
* @param {string} key - Metadata key.
|
|
103
|
-
* @param {string|Buffer} value - Value to append.
|
|
104
|
-
* @returns {Metadata} `this` for chaining.
|
|
105
|
-
*
|
|
106
|
-
* @example
|
|
107
|
-
* md.add('x-roles', 'admin');
|
|
108
|
-
* md.add('x-roles', 'editor');
|
|
109
|
-
* md.getAll('x-roles'); // ['admin', 'editor']
|
|
110
|
-
*/
|
|
111
|
-
add(key, value)
|
|
112
|
-
{
|
|
113
|
-
key = this._validateKey(key);
|
|
114
|
-
this._validateValue(key, value);
|
|
115
|
-
const existing = this._map.get(key);
|
|
116
|
-
if (existing) existing.push(value);
|
|
117
|
-
else this._map.set(key, [value]);
|
|
118
|
-
return this;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Get the first value for a metadata key.
|
|
123
|
-
*
|
|
124
|
-
* @param {string} key - Metadata key.
|
|
125
|
-
* @returns {string|Buffer|undefined} First value, or undefined if not set.
|
|
126
|
-
*
|
|
127
|
-
* @example
|
|
128
|
-
* md.get('x-request-id'); // 'abc-123'
|
|
129
|
-
*/
|
|
130
|
-
get(key)
|
|
131
|
-
{
|
|
132
|
-
key = normalizeKey(key);
|
|
133
|
-
const arr = this._map.get(key);
|
|
134
|
-
return arr ? arr[0] : undefined;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Get all values for a metadata key.
|
|
139
|
-
*
|
|
140
|
-
* @param {string} key - Metadata key.
|
|
141
|
-
* @returns {Array<string|Buffer>} Array of values (empty if key not set).
|
|
142
|
-
*
|
|
143
|
-
* @example
|
|
144
|
-
* md.getAll('x-roles'); // ['admin', 'editor']
|
|
145
|
-
*/
|
|
146
|
-
getAll(key)
|
|
147
|
-
{
|
|
148
|
-
key = normalizeKey(key);
|
|
149
|
-
return this._map.get(key) || [];
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Check whether a metadata key has been set.
|
|
154
|
-
*
|
|
155
|
-
* @param {string} key - Metadata key.
|
|
156
|
-
* @returns {boolean}
|
|
157
|
-
*/
|
|
158
|
-
has(key)
|
|
159
|
-
{
|
|
160
|
-
return this._map.has(normalizeKey(key));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Remove a metadata key and all its values.
|
|
165
|
-
*
|
|
166
|
-
* @param {string} key - Metadata key.
|
|
167
|
-
* @returns {boolean} `true` if the key existed and was removed.
|
|
168
|
-
*/
|
|
169
|
-
remove(key)
|
|
170
|
-
{
|
|
171
|
-
return this._map.delete(normalizeKey(key));
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Remove all metadata entries.
|
|
176
|
-
*
|
|
177
|
-
* @returns {Metadata} `this` for chaining.
|
|
178
|
-
*/
|
|
179
|
-
clear()
|
|
180
|
-
{
|
|
181
|
-
this._map.clear();
|
|
182
|
-
return this;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Return the number of distinct metadata keys.
|
|
187
|
-
*
|
|
188
|
-
* @returns {number}
|
|
189
|
-
*/
|
|
190
|
-
get size()
|
|
191
|
-
{
|
|
192
|
-
return this._map.size;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// -- Serialization -------------------------------------
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Convert metadata to a plain object suitable for HTTP/2 headers/trailers.
|
|
199
|
-
* Multi-valued keys are joined with `, `. Binary values are base64-encoded.
|
|
200
|
-
*
|
|
201
|
-
* @returns {Object<string, string>} Header-compatible object.
|
|
202
|
-
*
|
|
203
|
-
* @example
|
|
204
|
-
* md.toHeaders();
|
|
205
|
-
* // { 'x-request-id': 'abc-123', 'x-roles': 'admin, editor' }
|
|
206
|
-
*/
|
|
207
|
-
toHeaders()
|
|
208
|
-
{
|
|
209
|
-
const headers = {};
|
|
210
|
-
for (const [key, values] of this._map)
|
|
211
|
-
{
|
|
212
|
-
if (isBinaryKey(key))
|
|
213
|
-
{
|
|
214
|
-
// Binary keys: base64 encode each value, comma-join
|
|
215
|
-
headers[key] = values.map((v) =>
|
|
216
|
-
Buffer.isBuffer(v) ? v.toString('base64') : Buffer.from(String(v)).toString('base64')
|
|
217
|
-
).join(', ');
|
|
218
|
-
}
|
|
219
|
-
else
|
|
220
|
-
{
|
|
221
|
-
headers[key] = values.join(', ');
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
return headers;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Merge entries from another Metadata instance or plain object.
|
|
229
|
-
*
|
|
230
|
-
* @param {Metadata|Object<string, string|string[]>} other - Source to merge from.
|
|
231
|
-
* @returns {Metadata} `this` for chaining.
|
|
232
|
-
*/
|
|
233
|
-
merge(other)
|
|
234
|
-
{
|
|
235
|
-
if (other instanceof Metadata)
|
|
236
|
-
{
|
|
237
|
-
for (const [key, values] of other._map)
|
|
238
|
-
{
|
|
239
|
-
for (const v of values) this.add(key, v);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
else if (other && typeof other === 'object')
|
|
243
|
-
{
|
|
244
|
-
for (const [key, val] of Object.entries(other))
|
|
245
|
-
{
|
|
246
|
-
if (Array.isArray(val))
|
|
247
|
-
{
|
|
248
|
-
for (const v of val) this.add(key, v);
|
|
249
|
-
}
|
|
250
|
-
else
|
|
251
|
-
{
|
|
252
|
-
this.add(key, val);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
return this;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Create a shallow clone of this Metadata.
|
|
261
|
-
*
|
|
262
|
-
* @returns {Metadata} New Metadata instance with the same entries.
|
|
263
|
-
*/
|
|
264
|
-
clone()
|
|
265
|
-
{
|
|
266
|
-
const md = new Metadata({ maxSize: this._maxSize });
|
|
267
|
-
for (const [key, values] of this._map)
|
|
268
|
-
{
|
|
269
|
-
md._map.set(key, values.slice());
|
|
270
|
-
}
|
|
271
|
-
return md;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Iterate over all entries as `[key, value]` pairs (one entry per value).
|
|
276
|
-
*
|
|
277
|
-
* @yields {[string, string|Buffer]}
|
|
278
|
-
*/
|
|
279
|
-
*[Symbol.iterator]()
|
|
280
|
-
{
|
|
281
|
-
for (const [key, values] of this._map)
|
|
282
|
-
{
|
|
283
|
-
for (const v of values) yield [key, v];
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Return all entries as an array of `[key, value]` pairs.
|
|
289
|
-
*
|
|
290
|
-
* @returns {Array<[string, string|Buffer]>}
|
|
291
|
-
*/
|
|
292
|
-
entries()
|
|
293
|
-
{
|
|
294
|
-
const result = [];
|
|
295
|
-
for (const pair of this) result.push(pair);
|
|
296
|
-
return result;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Return all distinct keys.
|
|
301
|
-
*
|
|
302
|
-
* @returns {string[]}
|
|
303
|
-
*/
|
|
304
|
-
keys()
|
|
305
|
-
{
|
|
306
|
-
return Array.from(this._map.keys());
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// -- Validation ----------------------------------------
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Validate and normalize a metadata key.
|
|
313
|
-
* @private
|
|
314
|
-
* @param {string} key
|
|
315
|
-
* @returns {string} Normalized key.
|
|
316
|
-
*/
|
|
317
|
-
_validateKey(key)
|
|
318
|
-
{
|
|
319
|
-
if (typeof key !== 'string')
|
|
320
|
-
throw new TypeError('Metadata key must be a string');
|
|
321
|
-
|
|
322
|
-
key = key.toLowerCase().trim();
|
|
323
|
-
|
|
324
|
-
if (key.length === 0)
|
|
325
|
-
throw new Error('Metadata key must not be empty');
|
|
326
|
-
|
|
327
|
-
if (key.length > MAX_KEY_LENGTH)
|
|
328
|
-
throw new Error(`Metadata key exceeds max length (${MAX_KEY_LENGTH})`);
|
|
329
|
-
|
|
330
|
-
if (RESERVED.has(key))
|
|
331
|
-
throw new Error(`Cannot set reserved pseudo-header: ${key}`);
|
|
332
|
-
|
|
333
|
-
if (GRPC_INTERNAL.has(key))
|
|
334
|
-
throw new Error(`Cannot set gRPC internal header: ${key}`);
|
|
335
|
-
|
|
336
|
-
// Keys must be lowercase ASCII alphanumeric + hyphen + underscore + period
|
|
337
|
-
if (!/^[a-z0-9_.\-]+$/.test(key))
|
|
338
|
-
throw new Error(`Invalid metadata key: "${key}" (must be lowercase ASCII alphanumeric/hyphen/underscore/period)`);
|
|
339
|
-
|
|
340
|
-
return key;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Validate a metadata value.
|
|
345
|
-
* @private
|
|
346
|
-
* @param {string} key
|
|
347
|
-
* @param {string|Buffer} value
|
|
348
|
-
*/
|
|
349
|
-
_validateValue(key, value)
|
|
350
|
-
{
|
|
351
|
-
if (isBinaryKey(key))
|
|
352
|
-
{
|
|
353
|
-
if (!Buffer.isBuffer(value) && typeof value !== 'string')
|
|
354
|
-
throw new TypeError(`Binary metadata key "${key}" requires a Buffer or string value`);
|
|
355
|
-
}
|
|
356
|
-
else
|
|
357
|
-
{
|
|
358
|
-
if (typeof value !== 'string')
|
|
359
|
-
throw new TypeError(`Metadata key "${key}" requires a string value`);
|
|
360
|
-
|
|
361
|
-
// ASCII printable check (0x20-0x7E)
|
|
362
|
-
for (let i = 0; i < value.length; i++)
|
|
363
|
-
{
|
|
364
|
-
const c = value.charCodeAt(i);
|
|
365
|
-
if (c < 0x20 || c > 0x7E)
|
|
366
|
-
throw new Error(`Non-ASCII character in metadata value for key "${key}" at position ${i}`);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// -- Static Helpers ----------------------------------------
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Create a Metadata instance from HTTP/2 headers, extracting only user metadata
|
|
376
|
-
* (skipping pseudo-headers, gRPC internal headers, and standard HTTP headers).
|
|
377
|
-
*
|
|
378
|
-
* @param {Object<string, string|string[]>} headers - HTTP/2 headers object.
|
|
379
|
-
* @param {object} [opts] - Options.
|
|
380
|
-
* @param {number} [opts.maxSize=8192] - Maximum metadata size.
|
|
381
|
-
* @returns {Metadata} Populated metadata instance.
|
|
382
|
-
*
|
|
383
|
-
* @example
|
|
384
|
-
* const md = Metadata.fromHeaders(stream.headers);
|
|
385
|
-
*/
|
|
386
|
-
Metadata.fromHeaders = function fromHeaders(headers, opts)
|
|
387
|
-
{
|
|
388
|
-
const md = new Metadata(opts);
|
|
389
|
-
if (!headers || typeof headers !== 'object') return md;
|
|
390
|
-
|
|
391
|
-
for (const [key, rawValue] of Object.entries(headers))
|
|
392
|
-
{
|
|
393
|
-
const k = key.toLowerCase();
|
|
394
|
-
if (RESERVED.has(k) || GRPC_INTERNAL.has(k)) continue;
|
|
395
|
-
// Skip standard HTTP headers that aren't metadata
|
|
396
|
-
if (k === 'accept' || k === 'accept-encoding' || k === 'content-length') continue;
|
|
397
|
-
|
|
398
|
-
try
|
|
399
|
-
{
|
|
400
|
-
if (isBinaryKey(k))
|
|
401
|
-
{
|
|
402
|
-
// Base64-decode binary values
|
|
403
|
-
const values = String(rawValue).split(',').map((s) => s.trim());
|
|
404
|
-
for (const v of values)
|
|
405
|
-
{
|
|
406
|
-
md._map.set(k, md._map.get(k) || []);
|
|
407
|
-
md._map.get(k).push(Buffer.from(v, 'base64'));
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
else
|
|
411
|
-
{
|
|
412
|
-
const values = String(rawValue).split(',').map((s) => s.trim());
|
|
413
|
-
for (const v of values)
|
|
414
|
-
{
|
|
415
|
-
md._map.set(k, md._map.get(k) || []);
|
|
416
|
-
md._map.get(k).push(v);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
catch (e)
|
|
421
|
-
{
|
|
422
|
-
log.warn('skipping invalid metadata key=%s: %s', k, e.message);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
return md;
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
// -- Utility Functions -------------------------------------
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Check if a metadata key is a binary key (ends with `-bin`).
|
|
433
|
-
*
|
|
434
|
-
* @param {string} key - Metadata key.
|
|
435
|
-
* @returns {boolean}
|
|
436
|
-
*/
|
|
437
|
-
function isBinaryKey(key)
|
|
438
|
-
{
|
|
439
|
-
return key.endsWith('-bin');
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* Normalize a metadata key to lowercase.
|
|
444
|
-
*
|
|
445
|
-
* @param {string} key
|
|
446
|
-
* @returns {string}
|
|
447
|
-
*/
|
|
448
|
-
function normalizeKey(key)
|
|
449
|
-
{
|
|
450
|
-
return typeof key === 'string' ? key.toLowerCase().trim() : '';
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
module.exports = {
|
|
454
|
-
Metadata,
|
|
455
|
-
isBinaryKey,
|
|
456
|
-
normalizeKey,
|
|
457
|
-
RESERVED,
|
|
458
|
-
GRPC_INTERNAL,
|
|
459
|
-
MAX_KEY_LENGTH,
|
|
460
|
-
DEFAULT_MAX_METADATA_SIZE,
|
|
461
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* @module grpc/metadata
|
|
3
|
+
* @description gRPC metadata container — typed key-value pairs transmitted as
|
|
4
|
+
* HTTP/2 headers (initial metadata) and trailers (trailing metadata).
|
|
5
|
+
* Keys ending in `-bin` carry binary values (base64-encoded on the wire).
|
|
6
|
+
* All other keys carry ASCII string values.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const md = new Metadata();
|
|
10
|
+
* md.set('x-request-id', '123');
|
|
11
|
+
* md.add('x-tags', 'alpha');
|
|
12
|
+
* md.add('x-tags', 'beta');
|
|
13
|
+
* md.getAll('x-tags'); // ['alpha', 'beta']
|
|
14
|
+
*
|
|
15
|
+
* @example | Binary metadata
|
|
16
|
+
* md.set('icon-bin', Buffer.from([0x89, 0x50, 0x4e, 0x47]));
|
|
17
|
+
* md.get('icon-bin'); // <Buffer 89 50 4e 47>
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const log = require('../debug')('zero:grpc');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Reserved HTTP/2 pseudo-headers that must not be treated as gRPC metadata.
|
|
24
|
+
* @type {Set<string>}
|
|
25
|
+
* @private
|
|
26
|
+
*/
|
|
27
|
+
const RESERVED = new Set([':authority', ':method', ':path', ':scheme', ':status']);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Headers managed by the gRPC framing layer, not user metadata.
|
|
31
|
+
* @type {Set<string>}
|
|
32
|
+
* @private
|
|
33
|
+
*/
|
|
34
|
+
const GRPC_INTERNAL = new Set([
|
|
35
|
+
'content-type', 'te', 'grpc-timeout', 'grpc-encoding',
|
|
36
|
+
'grpc-accept-encoding', 'grpc-status', 'grpc-message',
|
|
37
|
+
'user-agent', 'host',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Maximum metadata key length (prevents abuse).
|
|
42
|
+
* @type {number}
|
|
43
|
+
*/
|
|
44
|
+
const MAX_KEY_LENGTH = 256;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Maximum total metadata size in bytes (soft limit — 8 KB default, configurable).
|
|
48
|
+
* @type {number}
|
|
49
|
+
*/
|
|
50
|
+
const DEFAULT_MAX_METADATA_SIZE = 8192;
|
|
51
|
+
|
|
52
|
+
// -- Metadata Class ----------------------------------------
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* gRPC metadata container — type-safe key-value pairs for headers and trailers.
|
|
56
|
+
*
|
|
57
|
+
* @class
|
|
58
|
+
*
|
|
59
|
+
* @param {object} [opts] - Configuration options.
|
|
60
|
+
* @param {number} [opts.maxSize=8192] - Maximum total serialized metadata size in bytes.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* const md = new Metadata();
|
|
64
|
+
* md.set('authorization', 'Bearer tok123');
|
|
65
|
+
* md.set('x-trace-id', crypto.randomUUID());
|
|
66
|
+
*/
|
|
67
|
+
class Metadata
|
|
68
|
+
{
|
|
69
|
+
constructor(opts = {})
|
|
70
|
+
{
|
|
71
|
+
/** @private */
|
|
72
|
+
this._map = new Map();
|
|
73
|
+
/** @private */
|
|
74
|
+
this._maxSize = opts.maxSize || DEFAULT_MAX_METADATA_SIZE;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// -- Core Operations -----------------------------------
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Set a metadata key to a single value, replacing any existing values.
|
|
81
|
+
* Binary keys (ending in `-bin`) accept Buffer values; all others accept strings.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} key - Metadata key (lowercase, no whitespace).
|
|
84
|
+
* @param {string|Buffer} value - The value to store.
|
|
85
|
+
* @returns {Metadata} `this` for chaining.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* md.set('x-request-id', 'abc-123');
|
|
89
|
+
*/
|
|
90
|
+
set(key, value)
|
|
91
|
+
{
|
|
92
|
+
key = this._validateKey(key);
|
|
93
|
+
this._validateValue(key, value);
|
|
94
|
+
this._map.set(key, [value]);
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Add a value to a metadata key without replacing existing values.
|
|
100
|
+
* Allows multi-valued metadata (e.g. multiple tags or roles).
|
|
101
|
+
*
|
|
102
|
+
* @param {string} key - Metadata key.
|
|
103
|
+
* @param {string|Buffer} value - Value to append.
|
|
104
|
+
* @returns {Metadata} `this` for chaining.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* md.add('x-roles', 'admin');
|
|
108
|
+
* md.add('x-roles', 'editor');
|
|
109
|
+
* md.getAll('x-roles'); // ['admin', 'editor']
|
|
110
|
+
*/
|
|
111
|
+
add(key, value)
|
|
112
|
+
{
|
|
113
|
+
key = this._validateKey(key);
|
|
114
|
+
this._validateValue(key, value);
|
|
115
|
+
const existing = this._map.get(key);
|
|
116
|
+
if (existing) existing.push(value);
|
|
117
|
+
else this._map.set(key, [value]);
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get the first value for a metadata key.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} key - Metadata key.
|
|
125
|
+
* @returns {string|Buffer|undefined} First value, or undefined if not set.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* md.get('x-request-id'); // 'abc-123'
|
|
129
|
+
*/
|
|
130
|
+
get(key)
|
|
131
|
+
{
|
|
132
|
+
key = normalizeKey(key);
|
|
133
|
+
const arr = this._map.get(key);
|
|
134
|
+
return arr ? arr[0] : undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get all values for a metadata key.
|
|
139
|
+
*
|
|
140
|
+
* @param {string} key - Metadata key.
|
|
141
|
+
* @returns {Array<string|Buffer>} Array of values (empty if key not set).
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* md.getAll('x-roles'); // ['admin', 'editor']
|
|
145
|
+
*/
|
|
146
|
+
getAll(key)
|
|
147
|
+
{
|
|
148
|
+
key = normalizeKey(key);
|
|
149
|
+
return this._map.get(key) || [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check whether a metadata key has been set.
|
|
154
|
+
*
|
|
155
|
+
* @param {string} key - Metadata key.
|
|
156
|
+
* @returns {boolean}
|
|
157
|
+
*/
|
|
158
|
+
has(key)
|
|
159
|
+
{
|
|
160
|
+
return this._map.has(normalizeKey(key));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Remove a metadata key and all its values.
|
|
165
|
+
*
|
|
166
|
+
* @param {string} key - Metadata key.
|
|
167
|
+
* @returns {boolean} `true` if the key existed and was removed.
|
|
168
|
+
*/
|
|
169
|
+
remove(key)
|
|
170
|
+
{
|
|
171
|
+
return this._map.delete(normalizeKey(key));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Remove all metadata entries.
|
|
176
|
+
*
|
|
177
|
+
* @returns {Metadata} `this` for chaining.
|
|
178
|
+
*/
|
|
179
|
+
clear()
|
|
180
|
+
{
|
|
181
|
+
this._map.clear();
|
|
182
|
+
return this;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Return the number of distinct metadata keys.
|
|
187
|
+
*
|
|
188
|
+
* @returns {number}
|
|
189
|
+
*/
|
|
190
|
+
get size()
|
|
191
|
+
{
|
|
192
|
+
return this._map.size;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// -- Serialization -------------------------------------
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Convert metadata to a plain object suitable for HTTP/2 headers/trailers.
|
|
199
|
+
* Multi-valued keys are joined with `, `. Binary values are base64-encoded.
|
|
200
|
+
*
|
|
201
|
+
* @returns {Object<string, string>} Header-compatible object.
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* md.toHeaders();
|
|
205
|
+
* // { 'x-request-id': 'abc-123', 'x-roles': 'admin, editor' }
|
|
206
|
+
*/
|
|
207
|
+
toHeaders()
|
|
208
|
+
{
|
|
209
|
+
const headers = {};
|
|
210
|
+
for (const [key, values] of this._map)
|
|
211
|
+
{
|
|
212
|
+
if (isBinaryKey(key))
|
|
213
|
+
{
|
|
214
|
+
// Binary keys: base64 encode each value, comma-join
|
|
215
|
+
headers[key] = values.map((v) =>
|
|
216
|
+
Buffer.isBuffer(v) ? v.toString('base64') : Buffer.from(String(v)).toString('base64')
|
|
217
|
+
).join(', ');
|
|
218
|
+
}
|
|
219
|
+
else
|
|
220
|
+
{
|
|
221
|
+
headers[key] = values.join(', ');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return headers;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Merge entries from another Metadata instance or plain object.
|
|
229
|
+
*
|
|
230
|
+
* @param {Metadata|Object<string, string|string[]>} other - Source to merge from.
|
|
231
|
+
* @returns {Metadata} `this` for chaining.
|
|
232
|
+
*/
|
|
233
|
+
merge(other)
|
|
234
|
+
{
|
|
235
|
+
if (other instanceof Metadata)
|
|
236
|
+
{
|
|
237
|
+
for (const [key, values] of other._map)
|
|
238
|
+
{
|
|
239
|
+
for (const v of values) this.add(key, v);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else if (other && typeof other === 'object')
|
|
243
|
+
{
|
|
244
|
+
for (const [key, val] of Object.entries(other))
|
|
245
|
+
{
|
|
246
|
+
if (Array.isArray(val))
|
|
247
|
+
{
|
|
248
|
+
for (const v of val) this.add(key, v);
|
|
249
|
+
}
|
|
250
|
+
else
|
|
251
|
+
{
|
|
252
|
+
this.add(key, val);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return this;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Create a shallow clone of this Metadata.
|
|
261
|
+
*
|
|
262
|
+
* @returns {Metadata} New Metadata instance with the same entries.
|
|
263
|
+
*/
|
|
264
|
+
clone()
|
|
265
|
+
{
|
|
266
|
+
const md = new Metadata({ maxSize: this._maxSize });
|
|
267
|
+
for (const [key, values] of this._map)
|
|
268
|
+
{
|
|
269
|
+
md._map.set(key, values.slice());
|
|
270
|
+
}
|
|
271
|
+
return md;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Iterate over all entries as `[key, value]` pairs (one entry per value).
|
|
276
|
+
*
|
|
277
|
+
* @yields {[string, string|Buffer]}
|
|
278
|
+
*/
|
|
279
|
+
*[Symbol.iterator]()
|
|
280
|
+
{
|
|
281
|
+
for (const [key, values] of this._map)
|
|
282
|
+
{
|
|
283
|
+
for (const v of values) yield [key, v];
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Return all entries as an array of `[key, value]` pairs.
|
|
289
|
+
*
|
|
290
|
+
* @returns {Array<[string, string|Buffer]>}
|
|
291
|
+
*/
|
|
292
|
+
entries()
|
|
293
|
+
{
|
|
294
|
+
const result = [];
|
|
295
|
+
for (const pair of this) result.push(pair);
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Return all distinct keys.
|
|
301
|
+
*
|
|
302
|
+
* @returns {string[]}
|
|
303
|
+
*/
|
|
304
|
+
keys()
|
|
305
|
+
{
|
|
306
|
+
return Array.from(this._map.keys());
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// -- Validation ----------------------------------------
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Validate and normalize a metadata key.
|
|
313
|
+
* @private
|
|
314
|
+
* @param {string} key
|
|
315
|
+
* @returns {string} Normalized key.
|
|
316
|
+
*/
|
|
317
|
+
_validateKey(key)
|
|
318
|
+
{
|
|
319
|
+
if (typeof key !== 'string')
|
|
320
|
+
throw new TypeError('Metadata key must be a string');
|
|
321
|
+
|
|
322
|
+
key = key.toLowerCase().trim();
|
|
323
|
+
|
|
324
|
+
if (key.length === 0)
|
|
325
|
+
throw new Error('Metadata key must not be empty');
|
|
326
|
+
|
|
327
|
+
if (key.length > MAX_KEY_LENGTH)
|
|
328
|
+
throw new Error(`Metadata key exceeds max length (${MAX_KEY_LENGTH})`);
|
|
329
|
+
|
|
330
|
+
if (RESERVED.has(key))
|
|
331
|
+
throw new Error(`Cannot set reserved pseudo-header: ${key}`);
|
|
332
|
+
|
|
333
|
+
if (GRPC_INTERNAL.has(key))
|
|
334
|
+
throw new Error(`Cannot set gRPC internal header: ${key}`);
|
|
335
|
+
|
|
336
|
+
// Keys must be lowercase ASCII alphanumeric + hyphen + underscore + period
|
|
337
|
+
if (!/^[a-z0-9_.\-]+$/.test(key))
|
|
338
|
+
throw new Error(`Invalid metadata key: "${key}" (must be lowercase ASCII alphanumeric/hyphen/underscore/period)`);
|
|
339
|
+
|
|
340
|
+
return key;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Validate a metadata value.
|
|
345
|
+
* @private
|
|
346
|
+
* @param {string} key
|
|
347
|
+
* @param {string|Buffer} value
|
|
348
|
+
*/
|
|
349
|
+
_validateValue(key, value)
|
|
350
|
+
{
|
|
351
|
+
if (isBinaryKey(key))
|
|
352
|
+
{
|
|
353
|
+
if (!Buffer.isBuffer(value) && typeof value !== 'string')
|
|
354
|
+
throw new TypeError(`Binary metadata key "${key}" requires a Buffer or string value`);
|
|
355
|
+
}
|
|
356
|
+
else
|
|
357
|
+
{
|
|
358
|
+
if (typeof value !== 'string')
|
|
359
|
+
throw new TypeError(`Metadata key "${key}" requires a string value`);
|
|
360
|
+
|
|
361
|
+
// ASCII printable check (0x20-0x7E)
|
|
362
|
+
for (let i = 0; i < value.length; i++)
|
|
363
|
+
{
|
|
364
|
+
const c = value.charCodeAt(i);
|
|
365
|
+
if (c < 0x20 || c > 0x7E)
|
|
366
|
+
throw new Error(`Non-ASCII character in metadata value for key "${key}" at position ${i}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// -- Static Helpers ----------------------------------------
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Create a Metadata instance from HTTP/2 headers, extracting only user metadata
|
|
376
|
+
* (skipping pseudo-headers, gRPC internal headers, and standard HTTP headers).
|
|
377
|
+
*
|
|
378
|
+
* @param {Object<string, string|string[]>} headers - HTTP/2 headers object.
|
|
379
|
+
* @param {object} [opts] - Options.
|
|
380
|
+
* @param {number} [opts.maxSize=8192] - Maximum metadata size.
|
|
381
|
+
* @returns {Metadata} Populated metadata instance.
|
|
382
|
+
*
|
|
383
|
+
* @example
|
|
384
|
+
* const md = Metadata.fromHeaders(stream.headers);
|
|
385
|
+
*/
|
|
386
|
+
Metadata.fromHeaders = function fromHeaders(headers, opts)
|
|
387
|
+
{
|
|
388
|
+
const md = new Metadata(opts);
|
|
389
|
+
if (!headers || typeof headers !== 'object') return md;
|
|
390
|
+
|
|
391
|
+
for (const [key, rawValue] of Object.entries(headers))
|
|
392
|
+
{
|
|
393
|
+
const k = key.toLowerCase();
|
|
394
|
+
if (RESERVED.has(k) || GRPC_INTERNAL.has(k)) continue;
|
|
395
|
+
// Skip standard HTTP headers that aren't metadata
|
|
396
|
+
if (k === 'accept' || k === 'accept-encoding' || k === 'content-length') continue;
|
|
397
|
+
|
|
398
|
+
try
|
|
399
|
+
{
|
|
400
|
+
if (isBinaryKey(k))
|
|
401
|
+
{
|
|
402
|
+
// Base64-decode binary values
|
|
403
|
+
const values = String(rawValue).split(',').map((s) => s.trim());
|
|
404
|
+
for (const v of values)
|
|
405
|
+
{
|
|
406
|
+
md._map.set(k, md._map.get(k) || []);
|
|
407
|
+
md._map.get(k).push(Buffer.from(v, 'base64'));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
else
|
|
411
|
+
{
|
|
412
|
+
const values = String(rawValue).split(',').map((s) => s.trim());
|
|
413
|
+
for (const v of values)
|
|
414
|
+
{
|
|
415
|
+
md._map.set(k, md._map.get(k) || []);
|
|
416
|
+
md._map.get(k).push(v);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch (e)
|
|
421
|
+
{
|
|
422
|
+
log.warn('skipping invalid metadata key=%s: %s', k, e.message);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return md;
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// -- Utility Functions -------------------------------------
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Check if a metadata key is a binary key (ends with `-bin`).
|
|
433
|
+
*
|
|
434
|
+
* @param {string} key - Metadata key.
|
|
435
|
+
* @returns {boolean}
|
|
436
|
+
*/
|
|
437
|
+
function isBinaryKey(key)
|
|
438
|
+
{
|
|
439
|
+
return key.endsWith('-bin');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Normalize a metadata key to lowercase.
|
|
444
|
+
*
|
|
445
|
+
* @param {string} key
|
|
446
|
+
* @returns {string}
|
|
447
|
+
*/
|
|
448
|
+
function normalizeKey(key)
|
|
449
|
+
{
|
|
450
|
+
return typeof key === 'string' ? key.toLowerCase().trim() : '';
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
module.exports = {
|
|
454
|
+
Metadata,
|
|
455
|
+
isBinaryKey,
|
|
456
|
+
normalizeKey,
|
|
457
|
+
RESERVED,
|
|
458
|
+
GRPC_INTERNAL,
|
|
459
|
+
MAX_KEY_LENGTH,
|
|
460
|
+
DEFAULT_MAX_METADATA_SIZE,
|
|
461
|
+
};
|