@theihtisham/mcp-server-firebase 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +362 -0
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.js +79 -0
  5. package/dist/services/firebase.d.ts +14 -0
  6. package/dist/services/firebase.js +163 -0
  7. package/dist/tools/auth.d.ts +3 -0
  8. package/dist/tools/auth.js +346 -0
  9. package/dist/tools/firestore.d.ts +3 -0
  10. package/dist/tools/firestore.js +802 -0
  11. package/dist/tools/functions.d.ts +3 -0
  12. package/dist/tools/functions.js +168 -0
  13. package/dist/tools/index.d.ts +10 -0
  14. package/dist/tools/index.js +30 -0
  15. package/dist/tools/messaging.d.ts +3 -0
  16. package/dist/tools/messaging.js +296 -0
  17. package/dist/tools/realtime-db.d.ts +4 -0
  18. package/dist/tools/realtime-db.js +271 -0
  19. package/dist/tools/storage.d.ts +3 -0
  20. package/dist/tools/storage.js +279 -0
  21. package/dist/tools/types.d.ts +11 -0
  22. package/dist/tools/types.js +3 -0
  23. package/dist/utils/cache.d.ts +16 -0
  24. package/dist/utils/cache.js +75 -0
  25. package/dist/utils/errors.d.ts +15 -0
  26. package/dist/utils/errors.js +94 -0
  27. package/dist/utils/index.d.ts +5 -0
  28. package/dist/utils/index.js +37 -0
  29. package/dist/utils/pagination.d.ts +28 -0
  30. package/dist/utils/pagination.js +75 -0
  31. package/dist/utils/validation.d.ts +22 -0
  32. package/dist/utils/validation.js +172 -0
  33. package/package.json +53 -0
  34. package/src/index.ts +94 -0
  35. package/src/services/firebase.ts +140 -0
  36. package/src/tools/auth.ts +375 -0
  37. package/src/tools/firestore.ts +931 -0
  38. package/src/tools/functions.ts +189 -0
  39. package/src/tools/index.ts +24 -0
  40. package/src/tools/messaging.ts +324 -0
  41. package/src/tools/realtime-db.ts +307 -0
  42. package/src/tools/storage.ts +314 -0
  43. package/src/tools/types.ts +10 -0
  44. package/src/utils/cache.ts +82 -0
  45. package/src/utils/errors.ts +110 -0
  46. package/src/utils/index.ts +4 -0
  47. package/src/utils/pagination.ts +105 -0
  48. package/src/utils/validation.ts +212 -0
  49. package/tests/cache.test.ts +139 -0
  50. package/tests/errors.test.ts +132 -0
  51. package/tests/firebase-service.test.ts +46 -0
  52. package/tests/pagination.test.ts +26 -0
  53. package/tests/tools.test.ts +226 -0
  54. package/tests/validation.test.ts +216 -0
  55. package/tsconfig.json +26 -0
  56. package/vitest.config.ts +15 -0
@@ -0,0 +1,3 @@
1
+ import type { ToolDefinition } from './types.js';
2
+ export declare const functionsTools: ToolDefinition[];
3
+ //# sourceMappingURL=functions.d.ts.map
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.functionsTools = void 0;
4
+ const index_js_1 = require("../utils/index.js");
5
+ // ============================================================
6
+ // CLOUD FUNCTIONS TOOLS
7
+ // ============================================================
8
+ exports.functionsTools = [
9
+ // ── functions_list ────────────────────────────────────
10
+ {
11
+ name: 'functions_list',
12
+ description: 'List deployed Firebase Cloud Functions with their trigger types and status.',
13
+ inputSchema: {
14
+ type: 'object',
15
+ properties: {
16
+ region: { type: 'string', description: 'Region filter (default: us-central1)' },
17
+ pageSize: { type: 'number', description: 'Maximum results (default: 100)' },
18
+ },
19
+ },
20
+ handler: async (args) => {
21
+ try {
22
+ const projectId = process.env['FIREBASE_PROJECT_ID'];
23
+ if (!projectId) {
24
+ throw new Error('FIREBASE_PROJECT_ID is required to list functions. ' +
25
+ 'Set it as an environment variable.');
26
+ }
27
+ const region = args['region'] || 'us-central1';
28
+ const pageSize = Math.min(args['pageSize'] || 100, 1000);
29
+ const { GoogleAuth } = await import('google-auth-library');
30
+ const auth = new GoogleAuth({
31
+ scopes: ['https://www.googleapis.com/auth/cloud-platform'],
32
+ });
33
+ const client = await auth.getClient();
34
+ const url = `https://cloudfunctions.googleapis.com/v2/projects/${projectId}/locations/${region}/functions?pageSize=${pageSize}`;
35
+ const response = await client.request({ url });
36
+ const data = response.data;
37
+ const functions = (data.functions || []).map((fn) => ({
38
+ name: fn.name?.split('/').pop(),
39
+ fullName: fn.name,
40
+ state: fn.state,
41
+ updateTime: fn.updateTime,
42
+ buildId: fn.buildId,
43
+ eventTrigger: fn.eventTrigger ? {
44
+ eventType: fn.eventTrigger.eventType,
45
+ resource: fn.eventTrigger.resource,
46
+ } : undefined,
47
+ httpsTrigger: fn.httpsTrigger ? {
48
+ uri: fn.httpsTrigger.uri,
49
+ } : undefined,
50
+ entryPoint: fn.entryPoint,
51
+ runtime: fn.runtime,
52
+ }));
53
+ return (0, index_js_1.formatSuccess)({
54
+ projectId,
55
+ region,
56
+ count: functions.length,
57
+ functions,
58
+ nextPageToken: data.nextPageToken,
59
+ });
60
+ }
61
+ catch (err) {
62
+ (0, index_js_1.handleFirebaseError)(err, 'functions', 'list');
63
+ }
64
+ },
65
+ },
66
+ // ── functions_trigger ─────────────────────────────────
67
+ {
68
+ name: 'functions_trigger',
69
+ description: 'Trigger an HTTP Cloud Function by URL. Sends a POST request with optional data payload.',
70
+ inputSchema: {
71
+ type: 'object',
72
+ properties: {
73
+ url: { type: 'string', description: 'HTTPS trigger URL of the Cloud Function' },
74
+ data: { description: 'JSON payload to send with the request' },
75
+ method: { type: 'string', enum: ['GET', 'POST', 'PUT', 'DELETE'], description: 'HTTP method (default: POST)' },
76
+ },
77
+ required: ['url'],
78
+ },
79
+ handler: async (args) => {
80
+ try {
81
+ const url = args['url'].trim();
82
+ const data = args['data'];
83
+ const method = args['method'] || 'POST';
84
+ if (!url.startsWith('https://')) {
85
+ throw new Error('URL must start with "https://".');
86
+ }
87
+ const { GoogleAuth } = await import('google-auth-library');
88
+ const auth = new GoogleAuth();
89
+ const client = await auth.getIdTokenClient(url);
90
+ const response = await client.request({
91
+ url,
92
+ method,
93
+ data,
94
+ responseType: 'json',
95
+ });
96
+ return (0, index_js_1.formatSuccess)({
97
+ status: response.status,
98
+ statusText: response.statusText,
99
+ data: response.data,
100
+ });
101
+ }
102
+ catch (err) {
103
+ (0, index_js_1.handleFirebaseError)(err, 'functions', 'trigger');
104
+ }
105
+ },
106
+ },
107
+ // ── functions_get_logs ────────────────────────────────
108
+ {
109
+ name: 'functions_get_logs',
110
+ description: 'Get recent logs for a Firebase Cloud Function. Requires FIREBASE_PROJECT_ID.',
111
+ inputSchema: {
112
+ type: 'object',
113
+ properties: {
114
+ functionName: { type: 'string', description: 'Name of the Cloud Function' },
115
+ region: { type: 'string', description: 'Region (default: us-central1)' },
116
+ pageSize: { type: 'number', description: 'Maximum log entries (default: 50, max 100)' },
117
+ filter: { type: 'string', description: 'Additional log filter expression' },
118
+ },
119
+ required: ['functionName'],
120
+ },
121
+ handler: async (args) => {
122
+ try {
123
+ const projectId = process.env['FIREBASE_PROJECT_ID'];
124
+ if (!projectId) {
125
+ throw new Error('FIREBASE_PROJECT_ID is required to get function logs.');
126
+ }
127
+ const functionName = args['functionName'].trim();
128
+ const region = args['region'] || 'us-central1';
129
+ const pageSize = Math.min(args['pageSize'] || 50, 100);
130
+ const additionalFilter = args['filter'] || '';
131
+ const { GoogleAuth } = await import('google-auth-library');
132
+ const auth = new GoogleAuth({
133
+ scopes: ['https://www.googleapis.com/auth/logging.read'],
134
+ });
135
+ const client = await auth.getClient();
136
+ const filter = `resource.type="cloud_function" resource.labels.function_name="${functionName}" resource.labels.region="${region}"${additionalFilter ? ` ${additionalFilter}` : ''}`;
137
+ const url = 'https://logging.googleapis.com/v2/entries:list';
138
+ const response = await client.request({
139
+ url,
140
+ method: 'POST',
141
+ data: {
142
+ resourceNames: [`projects/${projectId}`],
143
+ filter,
144
+ orderBy: 'timestamp desc',
145
+ pageSize,
146
+ },
147
+ });
148
+ const data = response.data;
149
+ const logs = (data.entries || []).map((entry) => ({
150
+ timestamp: entry.timestamp,
151
+ severity: entry.severity,
152
+ textPayload: entry.textPayload,
153
+ jsonPayload: entry.jsonPayload,
154
+ }));
155
+ return (0, index_js_1.formatSuccess)({
156
+ functionName,
157
+ region,
158
+ count: logs.length,
159
+ logs,
160
+ });
161
+ }
162
+ catch (err) {
163
+ (0, index_js_1.handleFirebaseError)(err, 'functions', 'get_logs');
164
+ }
165
+ },
166
+ },
167
+ ];
168
+ //# sourceMappingURL=functions.js.map
@@ -0,0 +1,10 @@
1
+ export type { ToolDefinition } from './types.js';
2
+ export { firestoreTools } from './firestore.js';
3
+ export { authTools } from './auth.js';
4
+ export { storageTools } from './storage.js';
5
+ export { realtimeDbTools } from './realtime-db.js';
6
+ export { functionsTools } from './functions.js';
7
+ export { messagingTools } from './messaging.js';
8
+ import type { ToolDefinition } from './types.js';
9
+ export declare const allTools: ToolDefinition[];
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.allTools = exports.messagingTools = exports.functionsTools = exports.realtimeDbTools = exports.storageTools = exports.authTools = exports.firestoreTools = void 0;
4
+ var firestore_js_1 = require("./firestore.js");
5
+ Object.defineProperty(exports, "firestoreTools", { enumerable: true, get: function () { return firestore_js_1.firestoreTools; } });
6
+ var auth_js_1 = require("./auth.js");
7
+ Object.defineProperty(exports, "authTools", { enumerable: true, get: function () { return auth_js_1.authTools; } });
8
+ var storage_js_1 = require("./storage.js");
9
+ Object.defineProperty(exports, "storageTools", { enumerable: true, get: function () { return storage_js_1.storageTools; } });
10
+ var realtime_db_js_1 = require("./realtime-db.js");
11
+ Object.defineProperty(exports, "realtimeDbTools", { enumerable: true, get: function () { return realtime_db_js_1.realtimeDbTools; } });
12
+ var functions_js_1 = require("./functions.js");
13
+ Object.defineProperty(exports, "functionsTools", { enumerable: true, get: function () { return functions_js_1.functionsTools; } });
14
+ var messaging_js_1 = require("./messaging.js");
15
+ Object.defineProperty(exports, "messagingTools", { enumerable: true, get: function () { return messaging_js_1.messagingTools; } });
16
+ const firestore_js_2 = require("./firestore.js");
17
+ const auth_js_2 = require("./auth.js");
18
+ const storage_js_2 = require("./storage.js");
19
+ const realtime_db_js_2 = require("./realtime-db.js");
20
+ const functions_js_2 = require("./functions.js");
21
+ const messaging_js_2 = require("./messaging.js");
22
+ exports.allTools = [
23
+ ...firestore_js_2.firestoreTools,
24
+ ...auth_js_2.authTools,
25
+ ...storage_js_2.storageTools,
26
+ ...realtime_db_js_2.realtimeDbTools,
27
+ ...functions_js_2.functionsTools,
28
+ ...messaging_js_2.messagingTools,
29
+ ];
30
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ import type { ToolDefinition } from './types.js';
2
+ export declare const messagingTools: ToolDefinition[];
3
+ //# sourceMappingURL=messaging.d.ts.map
@@ -0,0 +1,296 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.messagingTools = void 0;
4
+ const firebase_js_1 = require("../services/firebase.js");
5
+ const index_js_1 = require("../utils/index.js");
6
+ // ============================================================
7
+ // MESSAGING (FCM) TOOLS
8
+ // ============================================================
9
+ exports.messagingTools = [
10
+ // ── messaging_send ────────────────────────────────────
11
+ {
12
+ name: 'messaging_send',
13
+ description: 'Send a push notification to a specific device via FCM token or to a topic.',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ token: { type: 'string', description: 'FCM device registration token' },
18
+ topic: { type: 'string', description: 'Topic name (alternative to token)' },
19
+ condition: { type: 'string', description: 'Condition expression (alternative to token/topic)' },
20
+ notification: {
21
+ type: 'object',
22
+ properties: {
23
+ title: { type: 'string', description: 'Notification title' },
24
+ body: { type: 'string', description: 'Notification body text' },
25
+ imageUrl: { type: 'string', description: 'Notification image URL' },
26
+ },
27
+ description: 'Notification payload',
28
+ },
29
+ data: {
30
+ type: 'object',
31
+ description: 'Custom data payload (key-value pairs)',
32
+ },
33
+ android: {
34
+ type: 'object',
35
+ properties: {
36
+ priority: { type: 'string', enum: ['high', 'normal'], description: 'Message priority' },
37
+ ttl: { type: 'number', description: 'Time to live in seconds' },
38
+ },
39
+ description: 'Android-specific options',
40
+ },
41
+ apns: {
42
+ type: 'object',
43
+ properties: {
44
+ badge: { type: 'number', description: 'Badge count' },
45
+ sound: { type: 'string', description: 'Sound file name' },
46
+ },
47
+ description: 'APNs-specific options',
48
+ },
49
+ },
50
+ },
51
+ handler: async (args) => {
52
+ try {
53
+ const messaging = (0, firebase_js_1.getMessaging)();
54
+ const hasToken = !!args['token'];
55
+ const hasTopic = !!args['topic'];
56
+ const hasCondition = !!args['condition'];
57
+ if ([hasToken, hasTopic, hasCondition].filter(Boolean).length > 1) {
58
+ throw new Error('Provide exactly one of: token, topic, or condition.');
59
+ }
60
+ if (!hasToken && !hasTopic && !hasCondition) {
61
+ throw new Error('Must provide one of: token, topic, or condition.');
62
+ }
63
+ // Build message using the appropriate type
64
+ // We need to construct the message based on the target type
65
+ let message;
66
+ if (hasToken) {
67
+ message = { token: args['token'].trim() };
68
+ }
69
+ else if (hasTopic) {
70
+ message = { topic: args['topic'].trim() };
71
+ }
72
+ else {
73
+ message = { condition: args['condition'].trim() };
74
+ }
75
+ // Notification
76
+ if (args['notification']) {
77
+ const notif = args['notification'];
78
+ message.notification = {
79
+ title: notif.title || '',
80
+ body: notif.body || '',
81
+ ...(notif.imageUrl && { imageUrl: notif.imageUrl }),
82
+ };
83
+ }
84
+ // Data payload (values must be strings for FCM)
85
+ if (args['data']) {
86
+ const rawData = args['data'];
87
+ const stringData = {};
88
+ for (const [key, value] of Object.entries(rawData)) {
89
+ stringData[key] = String(value);
90
+ }
91
+ message.data = stringData;
92
+ }
93
+ // Android config
94
+ if (args['android']) {
95
+ const android = args['android'];
96
+ message.android = {
97
+ priority: android.priority || 'high',
98
+ ...(android.ttl !== undefined && { ttl: android.ttl * 1000 }),
99
+ };
100
+ }
101
+ // APNs config
102
+ if (args['apns']) {
103
+ const apns = args['apns'];
104
+ message.apns = {
105
+ payload: {
106
+ aps: {
107
+ ...(apns.badge !== undefined && { badge: apns.badge }),
108
+ ...(apns.sound && { sound: apns.sound }),
109
+ },
110
+ },
111
+ };
112
+ }
113
+ const messageId = await messaging.send(message);
114
+ return (0, index_js_1.formatSuccess)({
115
+ messageId,
116
+ target: hasToken ? `token:${args['token'].substring(0, 10)}...` :
117
+ hasTopic ? `topic:${args['topic']}` :
118
+ `condition:${args['condition']}`,
119
+ message: 'Notification sent successfully.',
120
+ });
121
+ }
122
+ catch (err) {
123
+ (0, index_js_1.handleFirebaseError)(err, 'messaging', 'send');
124
+ }
125
+ },
126
+ },
127
+ // ── messaging_send_multicast ──────────────────────────
128
+ {
129
+ name: 'messaging_send_multicast',
130
+ description: 'Send a push notification to multiple devices (up to 500 tokens) in a single request.',
131
+ inputSchema: {
132
+ type: 'object',
133
+ properties: {
134
+ tokens: {
135
+ type: 'array',
136
+ items: { type: 'string' },
137
+ description: 'Array of FCM device tokens (max 500)',
138
+ },
139
+ notification: {
140
+ type: 'object',
141
+ properties: {
142
+ title: { type: 'string' },
143
+ body: { type: 'string' },
144
+ imageUrl: { type: 'string' },
145
+ },
146
+ description: 'Notification payload',
147
+ },
148
+ data: { type: 'object', description: 'Custom data payload' },
149
+ },
150
+ required: ['tokens'],
151
+ },
152
+ handler: async (args) => {
153
+ try {
154
+ const tokens = args['tokens'];
155
+ if (!tokens || tokens.length === 0) {
156
+ throw new Error('Tokens array cannot be empty.');
157
+ }
158
+ if (tokens.length > 500) {
159
+ throw new Error(`Too many tokens: ${tokens.length}. Maximum is 500 per multicast. ` +
160
+ 'Split into multiple batches.');
161
+ }
162
+ const messaging = (0, firebase_js_1.getMessaging)();
163
+ const message = {
164
+ tokens: tokens.map((t) => t.trim()),
165
+ };
166
+ if (args['notification']) {
167
+ const notif = args['notification'];
168
+ message.notification = {
169
+ title: notif.title || '',
170
+ body: notif.body || '',
171
+ ...(notif.imageUrl && { imageUrl: notif.imageUrl }),
172
+ };
173
+ }
174
+ if (args['data']) {
175
+ const rawData = args['data'];
176
+ const stringData = {};
177
+ for (const [key, value] of Object.entries(rawData)) {
178
+ stringData[key] = String(value);
179
+ }
180
+ message.data = stringData;
181
+ }
182
+ const batchResponse = await messaging.sendEachForMulticast(message);
183
+ const failedTokens = [];
184
+ if (batchResponse.failureCount > 0) {
185
+ batchResponse.responses.forEach((resp, idx) => {
186
+ if (!resp.success) {
187
+ failedTokens.push(tokens[idx]);
188
+ }
189
+ });
190
+ }
191
+ return (0, index_js_1.formatSuccess)({
192
+ successCount: batchResponse.successCount,
193
+ failureCount: batchResponse.failureCount,
194
+ totalSent: tokens.length,
195
+ ...(failedTokens.length > 0 && {
196
+ failedTokens: failedTokens.slice(0, 50),
197
+ message: `${batchResponse.successCount} succeeded, ${batchResponse.failureCount} failed.`,
198
+ }),
199
+ });
200
+ }
201
+ catch (err) {
202
+ (0, index_js_1.handleFirebaseError)(err, 'messaging', 'send_multicast');
203
+ }
204
+ },
205
+ },
206
+ // ── messaging_subscribe_topic ─────────────────────────
207
+ {
208
+ name: 'messaging_subscribe_topic',
209
+ description: 'Subscribe device tokens to an FCM topic for topic-based messaging.',
210
+ inputSchema: {
211
+ type: 'object',
212
+ properties: {
213
+ tokens: {
214
+ type: 'array',
215
+ items: { type: 'string' },
216
+ description: 'Array of FCM tokens to subscribe (max 1000)',
217
+ },
218
+ topic: { type: 'string', description: 'Topic name to subscribe to' },
219
+ },
220
+ required: ['tokens', 'topic'],
221
+ },
222
+ handler: async (args) => {
223
+ try {
224
+ const tokens = args['tokens'];
225
+ const topic = args['topic'].trim();
226
+ if (!tokens || tokens.length === 0) {
227
+ throw new Error('Tokens array cannot be empty.');
228
+ }
229
+ if (tokens.length > 1000) {
230
+ throw new Error('Maximum 1000 tokens per subscribe request.');
231
+ }
232
+ if (!topic) {
233
+ throw new Error('Topic name cannot be empty.');
234
+ }
235
+ if (!/^[a-zA-Z0-9-_.~%]+$/.test(topic)) {
236
+ throw new Error(`Invalid topic name: "${topic}". ` +
237
+ 'Topic names must match: [a-zA-Z0-9-_.~%]+');
238
+ }
239
+ const messaging = (0, firebase_js_1.getMessaging)();
240
+ const response = await messaging.subscribeToTopic(tokens, topic);
241
+ return (0, index_js_1.formatSuccess)({
242
+ successCount: response.successCount,
243
+ failureCount: response.failureCount,
244
+ topic,
245
+ tokensRequested: tokens.length,
246
+ message: `${response.successCount} tokens subscribed to "${topic}".`,
247
+ });
248
+ }
249
+ catch (err) {
250
+ (0, index_js_1.handleFirebaseError)(err, 'messaging', 'subscribe_topic');
251
+ }
252
+ },
253
+ },
254
+ // ── messaging_unsubscribe_topic ───────────────────────
255
+ {
256
+ name: 'messaging_unsubscribe_topic',
257
+ description: 'Unsubscribe device tokens from an FCM topic.',
258
+ inputSchema: {
259
+ type: 'object',
260
+ properties: {
261
+ tokens: {
262
+ type: 'array',
263
+ items: { type: 'string' },
264
+ description: 'Array of FCM tokens to unsubscribe (max 1000)',
265
+ },
266
+ topic: { type: 'string', description: 'Topic name to unsubscribe from' },
267
+ },
268
+ required: ['tokens', 'topic'],
269
+ },
270
+ handler: async (args) => {
271
+ try {
272
+ const tokens = args['tokens'];
273
+ const topic = args['topic'].trim();
274
+ if (!tokens || tokens.length === 0) {
275
+ throw new Error('Tokens array cannot be empty.');
276
+ }
277
+ if (!topic) {
278
+ throw new Error('Topic name cannot be empty.');
279
+ }
280
+ const messaging = (0, firebase_js_1.getMessaging)();
281
+ const response = await messaging.unsubscribeFromTopic(tokens, topic);
282
+ return (0, index_js_1.formatSuccess)({
283
+ successCount: response.successCount,
284
+ failureCount: response.failureCount,
285
+ topic,
286
+ tokensRequested: tokens.length,
287
+ message: `${response.successCount} tokens unsubscribed from "${topic}".`,
288
+ });
289
+ }
290
+ catch (err) {
291
+ (0, index_js_1.handleFirebaseError)(err, 'messaging', 'unsubscribe_topic');
292
+ }
293
+ },
294
+ },
295
+ ];
296
+ //# sourceMappingURL=messaging.js.map
@@ -0,0 +1,4 @@
1
+ import type { ToolDefinition } from './types.js';
2
+ export declare function validateRtdbPath(path: string): string;
3
+ export declare const realtimeDbTools: ToolDefinition[];
4
+ //# sourceMappingURL=realtime-db.d.ts.map