@fireemu/win32-x64 0.1.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.
- package/LICENSE +202 -0
- package/README.md +17 -0
- package/THIRD_PARTY_LICENSES.txt +81 -0
- package/bin/fireemu.exe +0 -0
- package/bin/runner-node/blocking-error.mjs +46 -0
- package/bin/runner-node/blocking-response.mjs +85 -0
- package/bin/runner-node/callable-app-check-loader.mjs +90 -0
- package/bin/runner-node/callable-app-check.mjs +288 -0
- package/bin/runner-node/index.mjs +1117 -0
- package/bin/runner-node/log-context.mjs +128 -0
- package/package.json +27 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// Version-bounded loader instrumentation for the callable App Check options of
|
|
2
|
+
// `firebase-functions` (docs/specifications/firebase-app-check.md, section 13.4).
|
|
3
|
+
//
|
|
4
|
+
// `enforceAppCheck` and `consumeAppCheckToken` never reach the deployed endpoint: both v1 and
|
|
5
|
+
// v2 keep them inside the callable wrapper's closure, and `__endpoint.callableTrigger` is an
|
|
6
|
+
// empty object. Reading them therefore means observing a callable as it is declared, which is
|
|
7
|
+
// what this module does -- before any user code loads.
|
|
8
|
+
//
|
|
9
|
+
// It hooks three functions rather than the `onCall` exports, because that is the one place
|
|
10
|
+
// every spelling passes through:
|
|
11
|
+
//
|
|
12
|
+
// v2 onCall / onCallGenkit -> onCallHandler -> withInit -> wrapTraceContext
|
|
13
|
+
// v1 onCall / _onCallWithOptions / runWith(...).https.onCall / region(...).https.onCall
|
|
14
|
+
// -> onCallHandler -> wrapTraceContext
|
|
15
|
+
//
|
|
16
|
+
// The `onCall` exports themselves are not reachable everywhere: `firebase-functions/v1`
|
|
17
|
+
// re-exports `https` through non-configurable getters, and `onCallGenkit` calls the module's
|
|
18
|
+
// own `onCall` binding rather than the export. `onCallHandler` has no such escape hatch, and
|
|
19
|
+
// the options it receives are already resolved (global options folded in, `Expression`
|
|
20
|
+
// parameters evaluated), which is exactly what the daemon needs to see.
|
|
21
|
+
//
|
|
22
|
+
// Everything here is bounded by an explicit supported version range plus structural and
|
|
23
|
+
// behavioural probes. When any of them fails the answer is "undetermined", never a guess: the
|
|
24
|
+
// daemon then refuses to start Functions with App Check enabled.
|
|
25
|
+
|
|
26
|
+
import { readFileSync } from "node:fs";
|
|
27
|
+
import { createRequire, register } from "node:module";
|
|
28
|
+
import { dirname, join, parse as parsePath } from "node:path";
|
|
29
|
+
import { pathToFileURL } from "node:url";
|
|
30
|
+
|
|
31
|
+
/// `firebase-functions` majors whose callable internals this instrumentation is written for.
|
|
32
|
+
const SUPPORTED_MAJORS = [6, 7];
|
|
33
|
+
|
|
34
|
+
/// The one debug feature the trusted callable protocol turns on.
|
|
35
|
+
const REQUIRED_DEBUG_FEATURE = "skipTokenVerification";
|
|
36
|
+
|
|
37
|
+
/// The internal modules the hooks patch, relative to the package root.
|
|
38
|
+
const MODULES = {
|
|
39
|
+
commonHttps: "lib/common/providers/https.js",
|
|
40
|
+
trace: "lib/v2/trace.js",
|
|
41
|
+
onInit: "lib/common/onInit.js",
|
|
42
|
+
debug: "lib/common/debug.js",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const ESM_MODULES = {
|
|
46
|
+
commonHttps: "lib/esm/common/providers/https.mjs",
|
|
47
|
+
trace: "lib/esm/v2/trace.mjs",
|
|
48
|
+
onInit: "lib/esm/common/onInit.mjs",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const REGISTRY_SYMBOL = Symbol.for("fireemu.callableAppCheck");
|
|
52
|
+
|
|
53
|
+
/// The directory of the `firebase-functions` package that owns `entry`.
|
|
54
|
+
function packageRootOf(entry) {
|
|
55
|
+
let dir = dirname(entry);
|
|
56
|
+
const { root: filesystemRoot } = parsePath(dir);
|
|
57
|
+
for (;;) {
|
|
58
|
+
try {
|
|
59
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
60
|
+
if (pkg.name === "firebase-functions") return dir;
|
|
61
|
+
} catch {
|
|
62
|
+
// Not a package directory; keep walking up.
|
|
63
|
+
}
|
|
64
|
+
if (dir === filesystemRoot) break;
|
|
65
|
+
dir = dirname(dir);
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`no firebase-functions package directory above ${entry}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function unsupported(reason) {
|
|
71
|
+
return {
|
|
72
|
+
supported: false,
|
|
73
|
+
reason,
|
|
74
|
+
version: null,
|
|
75
|
+
debugFeatures: reason,
|
|
76
|
+
authHeaders: [],
|
|
77
|
+
optionsOf: () => undefined,
|
|
78
|
+
graphs: { commonjs: "unsupported", esm: "not-loaded" },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Whether `name` is an own data property of `target` that may be replaced.
|
|
83
|
+
function isPatchable(target, name) {
|
|
84
|
+
const d = Object.getOwnPropertyDescriptor(target, name);
|
|
85
|
+
return !!d && typeof d.value === "function" && d.writable !== false && d.configurable !== false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Checks that the installed package reads the debug switches the way the trusted protocol
|
|
89
|
+
/// relies on. `debugMode` is captured when `debug.js` is first required, so this observes the
|
|
90
|
+
/// environment the daemon actually handed to this process.
|
|
91
|
+
function probeDebugFeatures(debug) {
|
|
92
|
+
if (typeof debug.isDebugFeatureEnabled !== "function" || typeof debug.debugFeatureValue !== "function") {
|
|
93
|
+
return "the installed firebase-functions does not expose isDebugFeatureEnabled / debugFeatureValue";
|
|
94
|
+
}
|
|
95
|
+
let declared = {};
|
|
96
|
+
try {
|
|
97
|
+
declared = JSON.parse(process.env.FIREBASE_DEBUG_FEATURES ?? "{}") ?? {};
|
|
98
|
+
} catch {
|
|
99
|
+
declared = {};
|
|
100
|
+
}
|
|
101
|
+
const wanted = process.env.FIREBASE_DEBUG_MODE === "true" && declared[REQUIRED_DEBUG_FEATURE] === true;
|
|
102
|
+
if (debug.isDebugFeatureEnabled(REQUIRED_DEBUG_FEATURE) !== wanted) {
|
|
103
|
+
return `the installed firebase-functions reads ${REQUIRED_DEBUG_FEATURE} differently from FIREBASE_DEBUG_MODE / FIREBASE_DEBUG_FEATURES`;
|
|
104
|
+
}
|
|
105
|
+
if (debug.isDebugFeatureEnabled("ftdFeatureThatDoesNotExist") !== false) {
|
|
106
|
+
return "the installed firebase-functions enables debug features it was not asked for";
|
|
107
|
+
}
|
|
108
|
+
return "verified";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// How a declared `consumeAppCheckToken` maps onto the three-valued manifest field.
|
|
112
|
+
function consumeState(value) {
|
|
113
|
+
if (value === true) return "enabled";
|
|
114
|
+
if (value === false || value === undefined || value === null) return "disabled";
|
|
115
|
+
// A shape this instrumentation does not understand (an unevaluated parameter, say) is not
|
|
116
|
+
// evidence of `false`.
|
|
117
|
+
return "undetermined";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Installs the hooks and returns what the runner should report.
|
|
121
|
+
export function instrumentCallables(sourceDir) {
|
|
122
|
+
const require = createRequire(join(sourceDir, "package.json"));
|
|
123
|
+
let root;
|
|
124
|
+
let version;
|
|
125
|
+
try {
|
|
126
|
+
// `firebase-functions` does not publish `./package.json` in its `exports` map, so the
|
|
127
|
+
// package root is found from its resolved entry point instead of asked for by subpath.
|
|
128
|
+
root = packageRootOf(require.resolve("firebase-functions"));
|
|
129
|
+
version = String(JSON.parse(readFileSync(join(root, "package.json"), "utf8")).version ?? "");
|
|
130
|
+
} catch (e) {
|
|
131
|
+
return unsupported(`firebase-functions is not installed in ${sourceDir}: ${e?.message ?? e}`);
|
|
132
|
+
}
|
|
133
|
+
const major = Number.parseInt(version.split(".")[0], 10);
|
|
134
|
+
if (!SUPPORTED_MAJORS.includes(major)) {
|
|
135
|
+
return unsupported(
|
|
136
|
+
`firebase-functions ${version} is outside the supported range (major ${SUPPORTED_MAJORS.join(", ")})`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const loaded = {};
|
|
141
|
+
for (const [key, relative] of Object.entries(MODULES)) {
|
|
142
|
+
try {
|
|
143
|
+
// Required by absolute path on purpose: these are internal modules the package's
|
|
144
|
+
// `exports` map does not publish, and the realpath is the same module instance the
|
|
145
|
+
// provider modules themselves hold.
|
|
146
|
+
loaded[key] = require(join(root, relative));
|
|
147
|
+
} catch (e) {
|
|
148
|
+
return unsupported(
|
|
149
|
+
`firebase-functions ${version} has no ${relative}: ${e?.message ?? e}`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const debugFeatures = probeDebugFeatures(loaded.debug);
|
|
155
|
+
for (const [key, name] of [
|
|
156
|
+
["commonHttps", "onCallHandler"],
|
|
157
|
+
["trace", "wrapTraceContext"],
|
|
158
|
+
["onInit", "withInit"],
|
|
159
|
+
]) {
|
|
160
|
+
if (!isPatchable(loaded[key], name)) {
|
|
161
|
+
return {
|
|
162
|
+
...unsupported(`firebase-functions ${version} does not export a patchable ${name}`),
|
|
163
|
+
version,
|
|
164
|
+
debugFeatures,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
for (const name of ["unsafeDecodeAppCheckToken", "CALLABLE_AUTH_HEADER", "ORIGINAL_AUTH_HEADER"]) {
|
|
169
|
+
if (!(name in loaded.commonHttps)) {
|
|
170
|
+
return {
|
|
171
|
+
...unsupported(
|
|
172
|
+
`firebase-functions ${version} no longer defines ${name}; the callable trust boundary assumes it does`,
|
|
173
|
+
),
|
|
174
|
+
version,
|
|
175
|
+
debugFeatures,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// The *values*, not just the names. These are the fields the callable wrapper honours under
|
|
180
|
+
// `skipTokenVerification` to override v1 auth context, and the daemon strips them by name
|
|
181
|
+
// from every forwarded request. If a supported minor release renames one, the daemon's list
|
|
182
|
+
// silently goes stale, so the daemon compares this report against its own and refuses to
|
|
183
|
+
// start rather than forwarding a channel it no longer strips.
|
|
184
|
+
const authHeaders = [
|
|
185
|
+
loaded.commonHttps.CALLABLE_AUTH_HEADER,
|
|
186
|
+
loaded.commonHttps.ORIGINAL_AUTH_HEADER,
|
|
187
|
+
].map((v) => String(v ?? "").toLowerCase());
|
|
188
|
+
|
|
189
|
+
const options = new WeakMap();
|
|
190
|
+
const observe = (fn, opts) => {
|
|
191
|
+
if (typeof fn !== "function" || options.has(fn)) return;
|
|
192
|
+
options.set(
|
|
193
|
+
fn,
|
|
194
|
+
Object.freeze({
|
|
195
|
+
enforceAppCheck: (opts ?? {}).enforceAppCheck === true,
|
|
196
|
+
consumeAppCheckToken: consumeState((opts ?? {}).consumeAppCheckToken),
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
};
|
|
200
|
+
const carry = (from, to) => {
|
|
201
|
+
if (typeof to === "function" && !options.has(to) && options.has(from)) {
|
|
202
|
+
options.set(to, options.get(from));
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const originalOnCallHandler = loaded.commonHttps.onCallHandler;
|
|
207
|
+
loaded.commonHttps.onCallHandler = function onCallHandler(opts, handler, version_) {
|
|
208
|
+
const fn = originalOnCallHandler.call(this, opts, handler, version_);
|
|
209
|
+
observe(fn, opts);
|
|
210
|
+
return fn;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const originalWrapTrace = loaded.trace.wrapTraceContext;
|
|
214
|
+
loaded.trace.wrapTraceContext = function wrapTraceContext(handler) {
|
|
215
|
+
const fn = originalWrapTrace.call(this, handler);
|
|
216
|
+
carry(handler, fn);
|
|
217
|
+
return fn;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const originalWithInit = loaded.onInit.withInit;
|
|
221
|
+
loaded.onInit.withInit = function withInit(func) {
|
|
222
|
+
const fn = originalWithInit.call(this, func);
|
|
223
|
+
carry(func, fn);
|
|
224
|
+
return fn;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const esmLoaded = new Set();
|
|
228
|
+
const esmFailed = new Map();
|
|
229
|
+
const registry = Object.freeze({
|
|
230
|
+
observe,
|
|
231
|
+
carry,
|
|
232
|
+
moduleLoaded(name) {
|
|
233
|
+
esmLoaded.add(name);
|
|
234
|
+
},
|
|
235
|
+
moduleFailed(name, reason) {
|
|
236
|
+
esmFailed.set(name, reason);
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
Object.defineProperty(globalThis, REGISTRY_SYMBOL, {
|
|
240
|
+
value: registry,
|
|
241
|
+
writable: false,
|
|
242
|
+
configurable: false,
|
|
243
|
+
enumerable: false,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const esmTargets = Object.fromEntries(
|
|
247
|
+
Object.entries(ESM_MODULES).map(([name, relative]) => [
|
|
248
|
+
pathToFileURL(join(root, relative)).href,
|
|
249
|
+
name,
|
|
250
|
+
]),
|
|
251
|
+
);
|
|
252
|
+
register(new URL("./callable-app-check-loader.mjs", import.meta.url), {
|
|
253
|
+
data: { targets: esmTargets },
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const esmStatus = () => {
|
|
257
|
+
if (esmFailed.size > 0) {
|
|
258
|
+
return `failed: ${[...esmFailed.entries()]
|
|
259
|
+
.map(([name, reason]) => `${name} (${reason})`)
|
|
260
|
+
.join(", ")}`;
|
|
261
|
+
}
|
|
262
|
+
if (esmLoaded.size === 0) return "not-loaded";
|
|
263
|
+
if (esmLoaded.size !== Object.keys(ESM_MODULES).length) {
|
|
264
|
+
return `partial: ${[...esmLoaded].sort().join(", ")}`;
|
|
265
|
+
}
|
|
266
|
+
return "instrumented";
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
get supported() {
|
|
271
|
+
const status = esmStatus();
|
|
272
|
+
return status === "not-loaded" || status === "instrumented";
|
|
273
|
+
},
|
|
274
|
+
get reason() {
|
|
275
|
+
const status = esmStatus();
|
|
276
|
+
return status === "not-loaded" || status === "instrumented"
|
|
277
|
+
? null
|
|
278
|
+
: `firebase-functions ${version} ESM callable instrumentation ${status}`;
|
|
279
|
+
},
|
|
280
|
+
version,
|
|
281
|
+
debugFeatures,
|
|
282
|
+
authHeaders,
|
|
283
|
+
optionsOf: (fn) => options.get(fn),
|
|
284
|
+
get graphs() {
|
|
285
|
+
return { commonjs: "instrumented", esm: esmStatus() };
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|