oidc-spa 10.2.11 → 10.2.13
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/README.md +0 -24
- package/core/createOidc.js +4 -3
- package/core/createOidc.js.map +1 -1
- package/core/loginOrGoToAuthServer.js +18 -0
- package/core/loginOrGoToAuthServer.js.map +1 -1
- package/esm/core/createOidc.mjs +4 -3
- package/esm/core/createOidc.mjs.map +1 -1
- package/esm/core/loginOrGoToAuthServer.mjs +18 -0
- package/esm/core/loginOrGoToAuthServer.mjs.map +1 -1
- package/esm/tanstack-start/react/createOidcSpaUtils.mjs +26 -55
- package/esm/tanstack-start/react/createOidcSpaUtils.mjs.map +1 -1
- package/esm/tools/isNetworkError.d.ts +15 -0
- package/esm/tools/isNetworkError.mjs +18 -0
- package/esm/tools/isNetworkError.mjs.map +1 -0
- package/esm/vendor/build-runtime/babel.d.ts +5 -4
- package/esm/vendor/build-runtime/babel.mjs +4 -4
- package/esm/vite-plugin/handleTanstackStartBootstrapEnv.d.ts +11 -0
- package/esm/vite-plugin/handleTanstackStartBootstrapEnv.mjs +372 -0
- package/esm/vite-plugin/handleTanstackStartBootstrapEnv.mjs.map +1 -0
- package/esm/vite-plugin/vite-plugin.mjs +18 -2
- package/esm/vite-plugin/vite-plugin.mjs.map +1 -1
- package/package.json +1 -1
- package/src/core/createOidc.ts +3 -2
- package/src/core/loginOrGoToAuthServer.ts +19 -0
- package/src/tanstack-start/react/createOidcSpaUtils.ts +29 -87
- package/src/tanstack-start/react/publicEnv.d.ts +3 -0
- package/src/tools/isNetworkError.ts +17 -0
- package/src/vendor/build-runtime/babel.ts +13 -4
- package/src/vite-plugin/handleTanstackStartBootstrapEnv.ts +440 -0
- package/src/vite-plugin/vite-plugin.ts +20 -2
- package/tools/isNetworkError.d.ts +15 -0
- package/tools/isNetworkError.js +21 -0
- package/tools/isNetworkError.js.map +1 -0
|
@@ -16,7 +16,6 @@ import { assert, type Equals, is } from "../../tools/tsafe/assert";
|
|
|
16
16
|
import { createObjectThatThrowsIfAccessed } from "../../tools/createObjectThatThrowsIfAccessed";
|
|
17
17
|
import { createStatefulEvt } from "../../tools/StatefulEvt";
|
|
18
18
|
import { id } from "../../tools/tsafe/id";
|
|
19
|
-
import { typeGuard } from "../../tools/tsafe/typeGuard";
|
|
20
19
|
import type { GetterOrDirectValue } from "../../tools/GetterOrDirectValue";
|
|
21
20
|
import { createServerFn, createMiddleware } from "@tanstack/react-start";
|
|
22
21
|
// @ts-expect-error: Since our module is not labeled as ESM we don't have the types here.
|
|
@@ -541,95 +540,54 @@ export function createOidcSpaUtils<
|
|
|
541
540
|
assert(prModuleCore !== undefined);
|
|
542
541
|
|
|
543
542
|
const paramsOfBootstrap = await (async () => {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
543
|
+
class OidcSpaServerEnvRetrievalError extends Error {
|
|
544
|
+
constructor(params: { envName: string }) {
|
|
545
|
+
super(`oidc-spa: Env value ${params.envName} couldn't be pulled from server`);
|
|
546
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
547
549
|
|
|
548
|
-
const
|
|
549
|
-
get: (
|
|
550
|
+
const env_server_proxy = new Proxy(await fetchServerEnvVariableValues(), {
|
|
551
|
+
get: (target, envName) => {
|
|
550
552
|
assert(typeof envName === "string");
|
|
551
553
|
|
|
552
|
-
if (envName
|
|
553
|
-
|
|
554
|
+
if (!Object.prototype.hasOwnProperty.call(target, envName)) {
|
|
555
|
+
throw new OidcSpaServerEnvRetrievalError({ envName });
|
|
554
556
|
}
|
|
555
557
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
return "oidc_spa_probe";
|
|
558
|
+
return target[envName];
|
|
559
559
|
},
|
|
560
|
-
has: (
|
|
560
|
+
has: (target, envName) => {
|
|
561
561
|
assert(typeof envName === "string");
|
|
562
562
|
|
|
563
|
-
if (envName
|
|
564
|
-
|
|
563
|
+
if (!Object.prototype.hasOwnProperty.call(target, envName)) {
|
|
564
|
+
throw new OidcSpaServerEnvRetrievalError({ envName });
|
|
565
565
|
}
|
|
566
566
|
|
|
567
|
-
envNamesToPullFromServer.add(envName);
|
|
568
|
-
|
|
569
567
|
return true;
|
|
570
568
|
}
|
|
571
|
-
})
|
|
569
|
+
}) as Record<string, string>;
|
|
572
570
|
|
|
573
|
-
let
|
|
574
|
-
| {
|
|
575
|
-
hasThrown: false;
|
|
576
|
-
paramsOfBootstrap: ParamsOfBootstrap<
|
|
577
|
-
AutoLogin,
|
|
578
|
-
DecodedIdToken,
|
|
579
|
-
AccessTokenClaims
|
|
580
|
-
>;
|
|
581
|
-
}
|
|
582
|
-
| {
|
|
583
|
-
hasThrown: true;
|
|
584
|
-
error: unknown;
|
|
585
|
-
}
|
|
586
|
-
| undefined = undefined;
|
|
587
|
-
|
|
588
|
-
while (true) {
|
|
589
|
-
envNamesToPullFromServer = new Set();
|
|
590
|
-
|
|
591
|
-
result = undefined;
|
|
592
|
-
|
|
593
|
-
try {
|
|
594
|
-
const paramsOfBootstrap = getParamsOfBootstrap({ process: { env: env_proxy } });
|
|
595
|
-
result = {
|
|
596
|
-
hasThrown: false,
|
|
597
|
-
paramsOfBootstrap
|
|
598
|
-
};
|
|
599
|
-
} catch (error) {
|
|
600
|
-
result = {
|
|
601
|
-
hasThrown: true,
|
|
602
|
-
error
|
|
603
|
-
};
|
|
604
|
-
}
|
|
571
|
+
let paramsOfBootstrap: ParamsOfBootstrap<AutoLogin, DecodedIdToken, AccessTokenClaims>;
|
|
605
572
|
|
|
606
|
-
|
|
607
|
-
|
|
573
|
+
try {
|
|
574
|
+
paramsOfBootstrap = getParamsOfBootstrap({ process: { env: env_server_proxy } });
|
|
575
|
+
} catch (error) {
|
|
576
|
+
if (error instanceof OidcSpaServerEnvRetrievalError) {
|
|
577
|
+
throw error;
|
|
608
578
|
}
|
|
609
579
|
|
|
610
|
-
Object.entries(
|
|
611
|
-
await fetchServerEnvVariableValues({
|
|
612
|
-
data: {
|
|
613
|
-
envVarNames: Array.from(envNamesToPullFromServer)
|
|
614
|
-
}
|
|
615
|
-
})
|
|
616
|
-
).forEach(([envName, value]) => {
|
|
617
|
-
env[envName] = value;
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
if (result.hasThrown) {
|
|
622
580
|
throw new Error(
|
|
623
581
|
[
|
|
624
582
|
"oidc-spa: The function argument passed to bootstrapOidc",
|
|
625
583
|
"has thrown when invoked."
|
|
626
584
|
].join(" "),
|
|
627
585
|
//@ts-expect-error
|
|
628
|
-
{ cause:
|
|
586
|
+
{ cause: error }
|
|
629
587
|
);
|
|
630
588
|
}
|
|
631
589
|
|
|
632
|
-
return
|
|
590
|
+
return paramsOfBootstrap;
|
|
633
591
|
})();
|
|
634
592
|
|
|
635
593
|
dParamsOfBootstrap.resolve(paramsOfBootstrap);
|
|
@@ -1021,26 +979,10 @@ export function createOidcSpaUtils<
|
|
|
1021
979
|
};
|
|
1022
980
|
}
|
|
1023
981
|
|
|
1024
|
-
const fetchServerEnvVariableValues = createServerFn({ method: "GET" })
|
|
1025
|
-
|
|
1026
|
-
if (typeof data !== "object" || data === null) {
|
|
1027
|
-
throw new Error("Expected an object");
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
const { envVarNames } = data as Record<string, unknown>;
|
|
982
|
+
const fetchServerEnvVariableValues = createServerFn({ method: "GET" }).handler(async () => {
|
|
983
|
+
const { publicEnvNames } = await import("virtual:oidc-spa/tanstack-start-public-env");
|
|
1031
984
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
)
|
|
1037
|
-
);
|
|
1038
|
-
|
|
1039
|
-
return { envVarNames };
|
|
1040
|
-
})
|
|
1041
|
-
.handler(async ({ data }) => {
|
|
1042
|
-
const { envVarNames } = data;
|
|
1043
|
-
return Object.fromEntries(
|
|
1044
|
-
envVarNames.map(envVarName => [envVarName, process.env[envVarName] ?? ""])
|
|
1045
|
-
);
|
|
1046
|
-
});
|
|
985
|
+
return Object.fromEntries(
|
|
986
|
+
Array.from(publicEnvNames).map(envVarName => [envVarName, process.env[envVarName] ?? ""])
|
|
987
|
+
);
|
|
988
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tells whether an error thrown by `fetch()` (or by something that calls it,
|
|
3
|
+
* like oidc-client-ts, which re-throws it unwrapped) is a network failure.
|
|
4
|
+
*
|
|
5
|
+
* The Fetch spec requires a network failure to reject with a `TypeError`, but
|
|
6
|
+
* it does not specify the message, and every engine words it differently:
|
|
7
|
+
*
|
|
8
|
+
* - Chromium: "Failed to fetch"
|
|
9
|
+
* - Firefox: "NetworkError when attempting to fetch resource."
|
|
10
|
+
* - Safari: "Load failed"
|
|
11
|
+
*
|
|
12
|
+
* Matching on the text therefore only ever recognises one engine, and silently
|
|
13
|
+
* skips the recovery path everywhere else. Match on the type instead.
|
|
14
|
+
*/
|
|
15
|
+
export function isNetworkError(error: unknown): boolean {
|
|
16
|
+
return error instanceof TypeError;
|
|
17
|
+
}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
export * as babelParser from "@babel/parser";
|
|
2
|
-
import
|
|
3
|
-
export
|
|
2
|
+
import babelGenerate_import from "@babel/generator";
|
|
3
|
+
// Babel's CommonJS entrypoints expose their callable export under `default`.
|
|
4
|
+
// Native ESM and bundlers do not unwrap these imports in the same way.
|
|
5
|
+
export const babelGenerate: typeof babelGenerate_import =
|
|
6
|
+
typeof babelGenerate_import === "function"
|
|
7
|
+
? babelGenerate_import
|
|
8
|
+
: (babelGenerate_import as { default: typeof babelGenerate_import }).default;
|
|
4
9
|
export * as babelTypes from "@babel/types";
|
|
5
|
-
import
|
|
6
|
-
export
|
|
10
|
+
import babelTraverse_import from "@babel/traverse";
|
|
11
|
+
export const babelTraverse: typeof babelTraverse_import =
|
|
12
|
+
typeof babelTraverse_import === "function"
|
|
13
|
+
? babelTraverse_import
|
|
14
|
+
: (babelTraverse_import as { default: typeof babelTraverse_import }).default;
|
|
15
|
+
export type { NodePath } from "@babel/traverse";
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { PluginContext } from "rollup";
|
|
4
|
+
import type { ResolvedConfig, ViteDevServer } from "vite";
|
|
5
|
+
import { babelParser, babelTraverse, type NodePath } from "../vendor/build-runtime/babel";
|
|
6
|
+
|
|
7
|
+
/** The manifest contains names only; values are read by the server at request time. */
|
|
8
|
+
export function createHandleTanstackStartBootstrapEnv(params: { resolvedConfig: ResolvedConfig }) {
|
|
9
|
+
const { resolvedConfig } = params;
|
|
10
|
+
const virtualId = "virtual:oidc-spa/tanstack-start-public-env";
|
|
11
|
+
const resolvedId = `\0${virtualId}`;
|
|
12
|
+
const adapterId = "oidc-spa/react-tanstack-start";
|
|
13
|
+
let scanPromise: Promise<string[]> | undefined;
|
|
14
|
+
|
|
15
|
+
const isSourceFile = (id: string) => /\.[cm]?[jt]sx?$/.test(id) && !/\.d\.[cm]?ts$/.test(id);
|
|
16
|
+
|
|
17
|
+
const scan = async (context: Pick<PluginContext, "resolve" | "addWatchFile">) => {
|
|
18
|
+
type Source = {
|
|
19
|
+
program: NodePath;
|
|
20
|
+
imports: Map<string, string>;
|
|
21
|
+
};
|
|
22
|
+
const sources = new Map<string, Source>();
|
|
23
|
+
const excludedDirectories = new Set([
|
|
24
|
+
"node_modules",
|
|
25
|
+
"dist",
|
|
26
|
+
"build",
|
|
27
|
+
"coverage",
|
|
28
|
+
"public",
|
|
29
|
+
path.resolve(resolvedConfig.root, resolvedConfig.build.outDir),
|
|
30
|
+
path.resolve(resolvedConfig.cacheDir)
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
// Scan before generating the virtual module, including lazy routes. Collecting
|
|
34
|
+
// names in transform() would make authorization depend on module load order.
|
|
35
|
+
const discover = async (directory: string): Promise<string[]> => {
|
|
36
|
+
context.addWatchFile(directory);
|
|
37
|
+
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
38
|
+
const files = await Promise.all(
|
|
39
|
+
entries.map(async entry => {
|
|
40
|
+
const id = path.join(directory, entry.name);
|
|
41
|
+
if (
|
|
42
|
+
entry.name.startsWith(".") ||
|
|
43
|
+
excludedDirectories.has(entry.name) ||
|
|
44
|
+
excludedDirectories.has(id)
|
|
45
|
+
) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
if (entry.isDirectory()) return discover(id);
|
|
49
|
+
return entry.isFile() && isSourceFile(id) ? [id] : [];
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
return files.flat();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const pending = await discover(resolvedConfig.root);
|
|
56
|
+
for (let i = 0; i < pending.length; i++) {
|
|
57
|
+
const id = pending[i];
|
|
58
|
+
if (sources.has(id)) continue;
|
|
59
|
+
context.addWatchFile(id);
|
|
60
|
+
const code = await fs.readFile(id, "utf8");
|
|
61
|
+
const ast = babelParser.parse(code, {
|
|
62
|
+
sourceType: "unambiguous",
|
|
63
|
+
plugins: [
|
|
64
|
+
"typescript",
|
|
65
|
+
...(/\.[cm]?ts$/.test(id) ? [] : ["jsx" as const]),
|
|
66
|
+
"decorators-legacy"
|
|
67
|
+
]
|
|
68
|
+
});
|
|
69
|
+
let program!: NodePath;
|
|
70
|
+
babelTraverse(ast, {
|
|
71
|
+
Program(p) {
|
|
72
|
+
program = p;
|
|
73
|
+
p.stop();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
const source: Source = { program, imports: new Map() };
|
|
77
|
+
sources.set(id, source);
|
|
78
|
+
for (const statement of program.get("body") as NodePath[]) {
|
|
79
|
+
if (
|
|
80
|
+
!statement.isImportDeclaration() &&
|
|
81
|
+
!statement.isExportNamedDeclaration() &&
|
|
82
|
+
!statement.isExportAllDeclaration()
|
|
83
|
+
)
|
|
84
|
+
continue;
|
|
85
|
+
const specifier = statement.node.source?.value;
|
|
86
|
+
if (!specifier || specifier === adapterId) continue;
|
|
87
|
+
const resolved = await context.resolve(specifier, id, { skipSelf: true });
|
|
88
|
+
if (!resolved || resolved.external) continue;
|
|
89
|
+
const dependencyId = resolved.id.split("?")[0];
|
|
90
|
+
if (
|
|
91
|
+
!path.isAbsolute(dependencyId) ||
|
|
92
|
+
dependencyId.includes(`${path.sep}node_modules${path.sep}`) ||
|
|
93
|
+
!isSourceFile(dependencyId)
|
|
94
|
+
)
|
|
95
|
+
continue;
|
|
96
|
+
source.imports.set(specifier, dependencyId);
|
|
97
|
+
pending.push(dependencyId);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const names = new Set<string>();
|
|
102
|
+
const fail: (id: string, p: NodePath, reason: string) => never = (id, p, reason) => {
|
|
103
|
+
const loc = p.node.loc?.start;
|
|
104
|
+
throw new Error(
|
|
105
|
+
`oidc-spa: Cannot determine public bootstrap environment variables in ${id}${
|
|
106
|
+
loc ? `:${loc.line}:${loc.column + 1}` : ""
|
|
107
|
+
}. ${reason}`
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
const propertyName = (p: NodePath): string | undefined => {
|
|
111
|
+
if (!p.isMemberExpression() && !p.isOptionalMemberExpression() && !p.isObjectProperty())
|
|
112
|
+
return undefined;
|
|
113
|
+
const key = (p.isObjectProperty() ? p.get("key") : p.get("property")) as NodePath;
|
|
114
|
+
if (key.isStringLiteral()) return key.node.value;
|
|
115
|
+
if (!p.node.computed && key.isIdentifier()) return key.node.name;
|
|
116
|
+
return undefined;
|
|
117
|
+
};
|
|
118
|
+
const unwrap = (p: NodePath): NodePath => {
|
|
119
|
+
while (
|
|
120
|
+
p.isTSAsExpression() ||
|
|
121
|
+
p.isTSSatisfiesExpression() ||
|
|
122
|
+
p.isTSNonNullExpression() ||
|
|
123
|
+
p.isParenthesizedExpression()
|
|
124
|
+
)
|
|
125
|
+
p = p.get("expression") as NodePath;
|
|
126
|
+
return p;
|
|
127
|
+
};
|
|
128
|
+
type Origin = "namespace" | "builder" | "utils" | "bootstrap";
|
|
129
|
+
const memberOrigin = (
|
|
130
|
+
origin: Origin | undefined,
|
|
131
|
+
name: string | undefined
|
|
132
|
+
): Origin | undefined => {
|
|
133
|
+
if (origin === "namespace" && name === "oidcSpa") return "builder";
|
|
134
|
+
if (origin === "utils" && name === "bootstrapOidc") return "bootstrap";
|
|
135
|
+
return undefined;
|
|
136
|
+
};
|
|
137
|
+
const exportedOrigin = (id: string, name: string, seen: Set<object>): Origin | undefined => {
|
|
138
|
+
if (id === adapterId) return name === "oidcSpa" ? "builder" : undefined;
|
|
139
|
+
const source = sources.get(id);
|
|
140
|
+
if (!source) return undefined;
|
|
141
|
+
for (const statement of source.program.get("body") as NodePath[]) {
|
|
142
|
+
if (seen.has(statement.node)) continue;
|
|
143
|
+
if (statement.isExportDefaultDeclaration() && name === "default")
|
|
144
|
+
return origin(statement.get("declaration"), id, seen);
|
|
145
|
+
if (statement.isExportAllDeclaration()) {
|
|
146
|
+
const result = exportedOrigin(
|
|
147
|
+
source.imports.get(statement.node.source.value) ?? "",
|
|
148
|
+
name,
|
|
149
|
+
new Set([...seen, statement.node])
|
|
150
|
+
);
|
|
151
|
+
if (result) return result;
|
|
152
|
+
}
|
|
153
|
+
if (!statement.isExportNamedDeclaration()) continue;
|
|
154
|
+
const declaration = statement.get("declaration");
|
|
155
|
+
if (
|
|
156
|
+
declaration.node &&
|
|
157
|
+
source.program.scope.getBinding(name)?.path.findParent(p => p === statement)
|
|
158
|
+
) {
|
|
159
|
+
return bindingOrigin(source.program.scope.getBinding(name)!.path, name, id, seen);
|
|
160
|
+
}
|
|
161
|
+
for (const specifier of statement.get("specifiers")) {
|
|
162
|
+
if (!specifier.isExportSpecifier()) continue;
|
|
163
|
+
const exported = specifier.node.exported;
|
|
164
|
+
if ((exported.type === "Identifier" ? exported.name : exported.value) !== name)
|
|
165
|
+
continue;
|
|
166
|
+
if (!statement.node.source) return origin(specifier.get("local"), id, seen);
|
|
167
|
+
return exportedOrigin(
|
|
168
|
+
statement.node.source.value === adapterId
|
|
169
|
+
? adapterId
|
|
170
|
+
: source.imports.get(statement.node.source.value) ?? "",
|
|
171
|
+
specifier.node.local.name,
|
|
172
|
+
new Set([...seen, statement.node])
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return undefined;
|
|
177
|
+
};
|
|
178
|
+
const bindingOrigin = (
|
|
179
|
+
p: NodePath,
|
|
180
|
+
name: string,
|
|
181
|
+
id: string,
|
|
182
|
+
seen: Set<object>
|
|
183
|
+
): Origin | undefined => {
|
|
184
|
+
if (seen.has(p.node)) return undefined;
|
|
185
|
+
seen = new Set([...seen, p.node]);
|
|
186
|
+
if (
|
|
187
|
+
p.isImportSpecifier() ||
|
|
188
|
+
p.isImportDefaultSpecifier() ||
|
|
189
|
+
p.isImportNamespaceSpecifier()
|
|
190
|
+
) {
|
|
191
|
+
const declaration = p.parentPath;
|
|
192
|
+
if (!declaration?.isImportDeclaration()) return undefined;
|
|
193
|
+
const specifier = declaration.node.source.value;
|
|
194
|
+
if (p.isImportNamespaceSpecifier())
|
|
195
|
+
return specifier === adapterId ? "namespace" : undefined;
|
|
196
|
+
const imported = p.isImportSpecifier() ? p.node.imported : undefined;
|
|
197
|
+
return exportedOrigin(
|
|
198
|
+
specifier === adapterId ? adapterId : sources.get(id)?.imports.get(specifier) ?? "",
|
|
199
|
+
imported
|
|
200
|
+
? imported.type === "Identifier"
|
|
201
|
+
? imported.name
|
|
202
|
+
: imported.value
|
|
203
|
+
: "default",
|
|
204
|
+
seen
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (!p.isVariableDeclarator()) return undefined;
|
|
208
|
+
const valueOrigin = origin(p.get("init") as NodePath, id, seen);
|
|
209
|
+
const pattern = p.get("id");
|
|
210
|
+
if (pattern.isIdentifier()) return valueOrigin;
|
|
211
|
+
if (!pattern.isObjectPattern()) return undefined;
|
|
212
|
+
for (const property of pattern.get("properties")) {
|
|
213
|
+
if (property.isObjectProperty() && property.get("value").isIdentifier({ name }))
|
|
214
|
+
return memberOrigin(valueOrigin, propertyName(property));
|
|
215
|
+
}
|
|
216
|
+
return undefined;
|
|
217
|
+
};
|
|
218
|
+
const origin = (input: NodePath, id: string, seen = new Set<object>()): Origin | undefined => {
|
|
219
|
+
const p = unwrap(input);
|
|
220
|
+
if (!p.node) return undefined;
|
|
221
|
+
if (p.isIdentifier()) {
|
|
222
|
+
const binding = p.scope.getBinding(p.node.name);
|
|
223
|
+
return binding?.constant
|
|
224
|
+
? bindingOrigin(binding.path, p.node.name, id, seen)
|
|
225
|
+
: undefined;
|
|
226
|
+
}
|
|
227
|
+
if (p.isMemberExpression() || p.isOptionalMemberExpression())
|
|
228
|
+
return memberOrigin(origin(p.get("object") as NodePath, id, seen), propertyName(p));
|
|
229
|
+
if (!p.isCallExpression()) return undefined;
|
|
230
|
+
const callee = p.get("callee");
|
|
231
|
+
if (!callee.isMemberExpression() || origin(callee.get("object"), id, seen) !== "builder")
|
|
232
|
+
return undefined;
|
|
233
|
+
const method = propertyName(callee);
|
|
234
|
+
if (method === "createUtils") return "utils";
|
|
235
|
+
if (
|
|
236
|
+
[
|
|
237
|
+
"withAutoLogin",
|
|
238
|
+
"withExpectedDecodedIdTokenShape",
|
|
239
|
+
"withAccessTokenValidation"
|
|
240
|
+
].includes(method ?? "")
|
|
241
|
+
)
|
|
242
|
+
return "builder";
|
|
243
|
+
return undefined;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
for (const [id, source] of sources) {
|
|
247
|
+
source.program.traverse({
|
|
248
|
+
CallExpression(call) {
|
|
249
|
+
if (origin(call.get("callee"), id) !== "bootstrap") return;
|
|
250
|
+
let callback: NodePath = call.get("arguments")[0];
|
|
251
|
+
if (!callback) fail(id, call, "bootstrapOidc requires an argument.");
|
|
252
|
+
callback = unwrap(callback);
|
|
253
|
+
const seenCallbacks = new Set<object>();
|
|
254
|
+
while (callback.isIdentifier()) {
|
|
255
|
+
const binding = callback.scope.getBinding(callback.node.name);
|
|
256
|
+
if (!binding?.constant || seenCallbacks.has(binding.path.node)) break;
|
|
257
|
+
seenCallbacks.add(binding.path.node);
|
|
258
|
+
if (binding.path.isFunctionDeclaration()) {
|
|
259
|
+
callback = binding.path;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
if (!binding.path.isVariableDeclarator()) break;
|
|
263
|
+
callback = unwrap(binding.path.get("init") as NodePath);
|
|
264
|
+
}
|
|
265
|
+
// Direct configuration objects do not receive the env proxy.
|
|
266
|
+
if (callback.isObjectExpression()) return;
|
|
267
|
+
if (!callback.isFunction())
|
|
268
|
+
fail(
|
|
269
|
+
id,
|
|
270
|
+
callback,
|
|
271
|
+
"Use an inline callback or a locally declared callback so its environment accesses can be analyzed."
|
|
272
|
+
);
|
|
273
|
+
if (callback.node.async || callback.node.generator)
|
|
274
|
+
fail(id, callback, "The bootstrap callback must be synchronous.");
|
|
275
|
+
|
|
276
|
+
type ValueKind = "argument" | "process" | "env";
|
|
277
|
+
const visited = new Set<object>();
|
|
278
|
+
const followPattern = (pattern: NodePath, kind: ValueKind): void => {
|
|
279
|
+
if (pattern.isIdentifier()) {
|
|
280
|
+
const binding = pattern.scope.getBinding(pattern.node.name);
|
|
281
|
+
if (!binding || !binding.constant)
|
|
282
|
+
fail(id, pattern, "Do not reassign the bootstrap environment bindings.");
|
|
283
|
+
if (visited.has(binding)) return;
|
|
284
|
+
visited.add(binding);
|
|
285
|
+
binding.referencePaths.forEach(reference =>
|
|
286
|
+
followReference(reference, kind)
|
|
287
|
+
);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (!pattern.isObjectPattern())
|
|
291
|
+
fail(
|
|
292
|
+
id,
|
|
293
|
+
pattern,
|
|
294
|
+
"Use named properties when destructuring the bootstrap environment; rest and default bindings are unsupported."
|
|
295
|
+
);
|
|
296
|
+
for (const property of pattern.get("properties")) {
|
|
297
|
+
if (!property.isObjectProperty())
|
|
298
|
+
fail(id, property, "Do not spread the bootstrap environment.");
|
|
299
|
+
const name = propertyName(property);
|
|
300
|
+
if (name === undefined)
|
|
301
|
+
fail(
|
|
302
|
+
id,
|
|
303
|
+
property,
|
|
304
|
+
"Environment variable names must be literal properties."
|
|
305
|
+
);
|
|
306
|
+
if (kind === "env") {
|
|
307
|
+
names.add(name);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (name !== (kind === "argument" ? "process" : "env"))
|
|
311
|
+
fail(
|
|
312
|
+
id,
|
|
313
|
+
property,
|
|
314
|
+
"Only process.env is available in the bootstrap environment."
|
|
315
|
+
);
|
|
316
|
+
followPattern(
|
|
317
|
+
property.get("value"),
|
|
318
|
+
kind === "argument" ? "process" : "env"
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
const followReference = (reference: NodePath, kind: ValueKind): void => {
|
|
323
|
+
let parent = reference.parentPath;
|
|
324
|
+
while (
|
|
325
|
+
parent &&
|
|
326
|
+
(parent.isTSAsExpression() ||
|
|
327
|
+
parent.isTSSatisfiesExpression() ||
|
|
328
|
+
parent.isTSNonNullExpression() ||
|
|
329
|
+
parent.isParenthesizedExpression())
|
|
330
|
+
) {
|
|
331
|
+
reference = parent;
|
|
332
|
+
parent = reference.parentPath;
|
|
333
|
+
}
|
|
334
|
+
if (!parent) return;
|
|
335
|
+
if (parent.isMemberExpression() || parent.isOptionalMemberExpression()) {
|
|
336
|
+
if (parent.get("object") !== reference)
|
|
337
|
+
fail(
|
|
338
|
+
id,
|
|
339
|
+
reference,
|
|
340
|
+
"The bootstrap environment cannot be used as a property key."
|
|
341
|
+
);
|
|
342
|
+
const name = propertyName(parent);
|
|
343
|
+
if (name === undefined)
|
|
344
|
+
fail(
|
|
345
|
+
id,
|
|
346
|
+
parent,
|
|
347
|
+
'Use process.env.NAME or process.env["NAME"]; computed environment variable names cannot be authorized safely.'
|
|
348
|
+
);
|
|
349
|
+
if (kind === "env") {
|
|
350
|
+
const use = parent.parentPath;
|
|
351
|
+
if (
|
|
352
|
+
(use?.isAssignmentExpression() && use.get("left") === parent) ||
|
|
353
|
+
use?.isUpdateExpression() ||
|
|
354
|
+
use?.isUnaryExpression({ operator: "delete" })
|
|
355
|
+
)
|
|
356
|
+
fail(id, parent, "Do not mutate the bootstrap environment.");
|
|
357
|
+
names.add(name);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (name !== (kind === "argument" ? "process" : "env"))
|
|
361
|
+
fail(
|
|
362
|
+
id,
|
|
363
|
+
parent,
|
|
364
|
+
"Only process.env is available in the bootstrap environment."
|
|
365
|
+
);
|
|
366
|
+
followReference(parent, kind === "argument" ? "process" : "env");
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (parent.isVariableDeclarator() && parent.get("init") === reference) {
|
|
370
|
+
followPattern(parent.get("id"), kind);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (
|
|
374
|
+
kind === "env" &&
|
|
375
|
+
parent.isBinaryExpression({ operator: "in" }) &&
|
|
376
|
+
parent.get("right") === reference
|
|
377
|
+
) {
|
|
378
|
+
const name = parent.get("left");
|
|
379
|
+
if (!name.isStringLiteral())
|
|
380
|
+
fail(
|
|
381
|
+
id,
|
|
382
|
+
name,
|
|
383
|
+
"Use a string literal for environment membership checks."
|
|
384
|
+
);
|
|
385
|
+
names.add(name.node.value);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (parent.isTSTypeQuery()) return;
|
|
389
|
+
fail(
|
|
390
|
+
id,
|
|
391
|
+
reference,
|
|
392
|
+
"Do not pass, spread, or return the bootstrap environment object. Read its literal named variables inside the callback instead."
|
|
393
|
+
);
|
|
394
|
+
};
|
|
395
|
+
const parameter = callback.get("params")[0];
|
|
396
|
+
if (parameter) followPattern(parameter, "argument");
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
return [...names].sort();
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
resolveId: (id: string) => (id === virtualId ? resolvedId : null),
|
|
405
|
+
buildStart: async (context: Pick<PluginContext, "resolve" | "addWatchFile">) => {
|
|
406
|
+
// Each build (including watch rebuilds) gets its own complete snapshot.
|
|
407
|
+
scanPromise = scan(context);
|
|
408
|
+
await scanPromise;
|
|
409
|
+
},
|
|
410
|
+
load: async (id: string, context: Pick<PluginContext, "resolve" | "addWatchFile">) => {
|
|
411
|
+
if (id !== resolvedId) return null;
|
|
412
|
+
const names = await (scanPromise ??= scan(context));
|
|
413
|
+
return `export const publicEnvNames = new Set(${JSON.stringify(names)});\n`;
|
|
414
|
+
},
|
|
415
|
+
configureServer: (server: ViteDevServer) => {
|
|
416
|
+
const onSourceChange = (id: string) => {
|
|
417
|
+
if (!isSourceFile(id) || id.includes(`${path.sep}node_modules${path.sep}`)) return;
|
|
418
|
+
scanPromise = undefined;
|
|
419
|
+
// The virtual module has no real filename for Vite to invalidate.
|
|
420
|
+
// Revoke removed names as well as discovering newly added names.
|
|
421
|
+
for (const environment of Object.values(server.environments)) {
|
|
422
|
+
const module = environment.moduleGraph.getModuleById(resolvedId);
|
|
423
|
+
if (!module) continue;
|
|
424
|
+
environment.moduleGraph.invalidateModule(module);
|
|
425
|
+
environment.hot.send({ type: "full-reload" });
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
server.watcher
|
|
429
|
+
.on("add", onSourceChange)
|
|
430
|
+
.on("change", onSourceChange)
|
|
431
|
+
.on("unlink", onSourceChange);
|
|
432
|
+
server.httpServer?.once("close", () => {
|
|
433
|
+
server.watcher
|
|
434
|
+
.off("add", onSourceChange)
|
|
435
|
+
.off("change", onSourceChange)
|
|
436
|
+
.off("unlink", onSourceChange);
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
}
|