oidc-spa 8.5.4 → 8.6.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 (30) hide show
  1. package/core/createOidc.js +16 -1
  2. package/core/createOidc.js.map +1 -1
  3. package/core/tokenExfiltrationDefense.js +17 -44
  4. package/core/tokenExfiltrationDefense.js.map +1 -1
  5. package/core/tokenPlaceholderSubstitution.d.ts +2 -5
  6. package/core/tokenPlaceholderSubstitution.js +73 -38
  7. package/core/tokenPlaceholderSubstitution.js.map +1 -1
  8. package/esm/core/createOidc.js +16 -1
  9. package/esm/core/createOidc.js.map +1 -1
  10. package/esm/core/tokenExfiltrationDefense.js +18 -45
  11. package/esm/core/tokenExfiltrationDefense.js.map +1 -1
  12. package/esm/core/tokenPlaceholderSubstitution.d.ts +2 -5
  13. package/esm/core/tokenPlaceholderSubstitution.js +72 -37
  14. package/esm/core/tokenPlaceholderSubstitution.js.map +1 -1
  15. package/esm/react-spa/createOidcSpaApi.js +3 -4
  16. package/esm/react-spa/createOidcSpaApi.js.map +1 -1
  17. package/esm/tanstack-start/react/createOidcSpaApi.js.map +1 -1
  18. package/esm/tanstack-start/react/disableSsrIfLoginEnforced.js.map +1 -1
  19. package/package.json +1 -1
  20. package/react-spa/createOidcSpaApi.js +2 -3
  21. package/react-spa/createOidcSpaApi.js.map +1 -1
  22. package/src/core/createOidc.ts +18 -0
  23. package/src/core/tokenExfiltrationDefense.ts +18 -45
  24. package/src/core/tokenPlaceholderSubstitution.ts +98 -45
  25. package/src/react-spa/{createOidcSpaApi.tsx → createOidcSpaApi.ts} +10 -3
  26. package/src/vite-plugin/handleClientEntrypoint.ts +142 -25
  27. package/vite-plugin/handleClientEntrypoint.js +104 -18
  28. package/vite-plugin/handleClientEntrypoint.js.map +1 -1
  29. /package/src/tanstack-start/react/{createOidcSpaApi.tsx → createOidcSpaApi.ts} +0 -0
  30. /package/src/tanstack-start/react/{disableSsrIfLoginEnforced.tsx → disableSsrIfLoginEnforced.ts} +0 -0
@@ -1,6 +1,6 @@
1
1
  import { assert } from "../tools/tsafe/assert";
2
2
  import {
3
- markTokenSubstitutionAdEnabled,
3
+ markTokenSubstitutionAsEnabled,
4
4
  substitutePlaceholderByRealToken
5
5
  } from "./tokenPlaceholderSubstitution";
6
6
  import { getIsHostnameAuthorized } from "../tools/isHostnameAuthorized";
@@ -15,7 +15,7 @@ const viteHashedJsAssetPathRegExp = /\/assets\/[^/]+-[a-zA-Z0-9_-]{8}\.js$/;
15
15
  export function enableTokenExfiltrationDefense(params: Params) {
16
16
  const { resourceServersAllowedHostnames = [], serviceWorkersAllowedHostnames = [] } = params;
17
17
 
18
- markTokenSubstitutionAdEnabled();
18
+ markTokenSubstitutionAsEnabled();
19
19
 
20
20
  patchFetchApiToSubstituteTokenPlaceholder({ resourceServersAllowedHostnames });
21
21
  patchXMLHttpRequestApiToSubstituteTokenPlaceholder({ resourceServersAllowedHostnames });
@@ -51,10 +51,7 @@ function patchFetchApiToSubstituteTokenPlaceholder(params: {
51
51
 
52
52
  const headers = new Headers();
53
53
  request.headers.forEach((value, key) => {
54
- const nextValue = substitutePlaceholderByRealToken({
55
- text: value,
56
- doEncodeUriComponent: false
57
- });
54
+ const nextValue = substitutePlaceholderByRealToken(value);
58
55
 
59
56
  if (nextValue !== value) {
60
57
  didSubstitute = true;
@@ -80,10 +77,7 @@ function patchFetchApiToSubstituteTokenPlaceholder(params: {
80
77
  }
81
78
 
82
79
  if (typeof init.body === "string") {
83
- body = substitutePlaceholderByRealToken({
84
- text: init.body,
85
- doEncodeUriComponent: false
86
- });
80
+ body = substitutePlaceholderByRealToken(init.body);
87
81
 
88
82
  if (init.body !== body) {
89
83
  didSubstitute = true;
@@ -97,10 +91,7 @@ function patchFetchApiToSubstituteTokenPlaceholder(params: {
97
91
  const next = new URLSearchParams();
98
92
 
99
93
  init.body.forEach((value, key) => {
100
- const nextValue = substitutePlaceholderByRealToken({
101
- text: value,
102
- doEncodeUriComponent: false
103
- });
94
+ const nextValue = substitutePlaceholderByRealToken(value);
104
95
 
105
96
  if (nextValue !== value) {
106
97
  didUrlSearchParamsSubstitute = true;
@@ -124,10 +115,7 @@ function patchFetchApiToSubstituteTokenPlaceholder(params: {
124
115
 
125
116
  init.body.forEach((value, key) => {
126
117
  if (typeof value === "string") {
127
- const nextValue = substitutePlaceholderByRealToken({
128
- text: value,
129
- doEncodeUriComponent: false
130
- });
118
+ const nextValue = substitutePlaceholderByRealToken(value);
131
119
 
132
120
  if (nextValue !== value) {
133
121
  didFormDataSubstitute = true;
@@ -200,10 +188,7 @@ function patchFetchApiToSubstituteTokenPlaceholder(params: {
200
188
  }
201
189
 
202
190
  const bodyText = await request.clone().text();
203
- const nextBodyText = substitutePlaceholderByRealToken({
204
- text: bodyText,
205
- doEncodeUriComponent: false
206
- });
191
+ const nextBodyText = substitutePlaceholderByRealToken(bodyText);
207
192
 
208
193
  if (nextBodyText !== bodyText) {
209
194
  didSubstitute = true;
@@ -217,7 +202,7 @@ function patchFetchApiToSubstituteTokenPlaceholder(params: {
217
202
  {
218
203
  const url_before = request.url;
219
204
 
220
- url = substitutePlaceholderByRealToken({ text: url_before, doEncodeUriComponent: true });
205
+ url = substitutePlaceholderByRealToken(url_before);
221
206
 
222
207
  if (url !== url_before) {
223
208
  didSubstitute = true;
@@ -301,7 +286,7 @@ function patchXMLHttpRequestApiToSubstituteTokenPlaceholder(params: {
301
286
 
302
287
  {
303
288
  const url_str = typeof url === "string" ? url : url.href;
304
- state.url = substitutePlaceholderByRealToken({ text: url_str, doEncodeUriComponent: true });
289
+ state.url = substitutePlaceholderByRealToken(url_str);
305
290
  if (url_str !== state.url) {
306
291
  state.didSubstitute = true;
307
292
  }
@@ -331,7 +316,7 @@ function patchXMLHttpRequestApiToSubstituteTokenPlaceholder(params: {
331
316
 
332
317
  assert(state !== undefined, "29440283");
333
318
 
334
- const nextValue = substitutePlaceholderByRealToken({ text: value, doEncodeUriComponent: false });
319
+ const nextValue = substitutePlaceholderByRealToken(value);
335
320
 
336
321
  if (nextValue !== value) {
337
322
  state.didSubstitute = true;
@@ -348,10 +333,7 @@ function patchXMLHttpRequestApiToSubstituteTokenPlaceholder(params: {
348
333
  let nextBody = body;
349
334
 
350
335
  if (typeof body === "string") {
351
- const nextBodyText = substitutePlaceholderByRealToken({
352
- text: body,
353
- doEncodeUriComponent: false
354
- });
336
+ const nextBodyText = substitutePlaceholderByRealToken(body);
355
337
 
356
338
  if (nextBodyText !== body) {
357
339
  state.didSubstitute = true;
@@ -409,7 +391,7 @@ function patchWebSocketApiToSubstituteTokenPlaceholder(params: {
409
391
 
410
392
  const WebSocketPatched = function WebSocket(url: string | URL, protocols?: string | string[]) {
411
393
  const urlStr = typeof url === "string" ? url : url.href;
412
- const nextUrl = substitutePlaceholderByRealToken({ text: urlStr, doEncodeUriComponent: true });
394
+ const nextUrl = substitutePlaceholderByRealToken(urlStr);
413
395
  let didSubstitute = nextUrl !== urlStr;
414
396
 
415
397
  const { hostname, pathname } = new URL(nextUrl, window.location.href);
@@ -474,10 +456,7 @@ function patchWebSocketApiToSubstituteTokenPlaceholder(params: {
474
456
  let nextData = data;
475
457
 
476
458
  if (typeof data === "string") {
477
- const nextDataText = substitutePlaceholderByRealToken({
478
- text: data,
479
- doEncodeUriComponent: false
480
- });
459
+ const nextDataText = substitutePlaceholderByRealToken(data);
481
460
 
482
461
  if (nextDataText !== data) {
483
462
  wsData.didSubstitute = true;
@@ -538,7 +517,7 @@ function patchEventSourceApiToSubstituteTokenPlaceholder(params: {
538
517
  eventSourceInitDict?: EventSourceInit
539
518
  ) {
540
519
  const urlStr = typeof url === "string" ? url : url.href;
541
- const nextUrl = substitutePlaceholderByRealToken({ text: urlStr, doEncodeUriComponent: true });
520
+ const nextUrl = substitutePlaceholderByRealToken(urlStr);
542
521
  const didSubstitute = nextUrl !== urlStr;
543
522
 
544
523
  const { hostname } = new URL(nextUrl, window.location.href);
@@ -599,7 +578,7 @@ function patchNavigatorSendBeaconApiToSubstituteTokenPlaceholder(params: {
599
578
 
600
579
  navigator.sendBeacon = function sendBeacon(url: string | URL, data?: BodyInit | null) {
601
580
  const urlStr = typeof url === "string" ? url : url.href;
602
- const nextUrl = substitutePlaceholderByRealToken({ text: urlStr, doEncodeUriComponent: true });
581
+ const nextUrl = substitutePlaceholderByRealToken(urlStr);
603
582
  let didSubstitute = nextUrl !== urlStr;
604
583
 
605
584
  const { hostname } = new URL(nextUrl, window.location.href);
@@ -607,7 +586,7 @@ function patchNavigatorSendBeaconApiToSubstituteTokenPlaceholder(params: {
607
586
  let nextData = data;
608
587
 
609
588
  if (typeof data === "string") {
610
- const next = substitutePlaceholderByRealToken({ text: data, doEncodeUriComponent: false });
589
+ const next = substitutePlaceholderByRealToken(data);
611
590
 
612
591
  if (next !== data) {
613
592
  didSubstitute = true;
@@ -619,10 +598,7 @@ function patchNavigatorSendBeaconApiToSubstituteTokenPlaceholder(params: {
619
598
  const next = new URLSearchParams();
620
599
 
621
600
  data.forEach((value, key) => {
622
- const nextValue = substitutePlaceholderByRealToken({
623
- text: value,
624
- doEncodeUriComponent: false
625
- });
601
+ const nextValue = substitutePlaceholderByRealToken(value);
626
602
 
627
603
  if (nextValue !== value) {
628
604
  didUrlSearchParamsSubstitute = true;
@@ -641,10 +617,7 @@ function patchNavigatorSendBeaconApiToSubstituteTokenPlaceholder(params: {
641
617
 
642
618
  data.forEach((value, key) => {
643
619
  if (typeof value === "string") {
644
- const nextValue = substitutePlaceholderByRealToken({
645
- text: value,
646
- doEncodeUriComponent: false
647
- });
620
+ const nextValue = substitutePlaceholderByRealToken(value);
648
621
 
649
622
  if (nextValue !== value) {
650
623
  didFormDataSubstitute = true;
@@ -2,7 +2,7 @@ import { assert } from "../tools/tsafe/assert";
2
2
 
3
3
  let isTokenSubstitutionEnabled = false;
4
4
 
5
- export function markTokenSubstitutionAdEnabled() {
5
+ export function markTokenSubstitutionAsEnabled() {
6
6
  isTokenSubstitutionEnabled = true;
7
7
  }
8
8
 
@@ -18,10 +18,61 @@ type Tokens = {
18
18
 
19
19
  const entries: {
20
20
  configId: string;
21
- tokens: Tokens;
22
21
  id: number;
22
+ tokens: Tokens;
23
+ tokens_placeholder: Tokens;
23
24
  }[] = [];
24
25
 
26
+ function generatePlaceholderForToken(params: {
27
+ tokenType: "id_token" | "access_token" | "refresh_token";
28
+ token_real: string;
29
+ id: number;
30
+ }): string {
31
+ const { tokenType, token_real, id } = params;
32
+
33
+ const match = token_real.match(/^([A-Za-z0-9\-_]+)\.([A-Za-z0-9\-_]+)\.([A-Za-z0-9\-_]+)$/);
34
+
35
+ if (match === null) {
36
+ assert(tokenType !== "id_token", "39232932927");
37
+ return `${tokenType}_placeholder_${id}`;
38
+ }
39
+
40
+ const [, header_b64, payload_b64, signature_b64] = match;
41
+
42
+ const signatureByteLength = (() => {
43
+ const b64 = signature_b64
44
+ .replace(/-/g, "+")
45
+ .replace(/_/g, "/")
46
+ .padEnd(Math.ceil(signature_b64.length / 4) * 4, "=");
47
+
48
+ const padding = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
49
+ return (b64.length * 3) / 4 - padding;
50
+ })();
51
+
52
+ const targetSigB64Length = Math.ceil((signatureByteLength * 4) / 3);
53
+
54
+ const sig_placeholder = (function makeZeroPaddedBase64UrlString(
55
+ targetLength: number,
56
+ seed: string
57
+ ): string {
58
+ const PAD = "A";
59
+
60
+ let out = seed.slice(0, targetLength);
61
+
62
+ if (out.length < targetLength) {
63
+ out = out + PAD.repeat(targetLength - out.length);
64
+ }
65
+
66
+ if (out.length % 4 === 1) {
67
+ out = out.slice(0, -1) + PAD;
68
+ }
69
+
70
+ return out;
71
+ })(targetSigB64Length, `sig_placeholder_${id}_`);
72
+
73
+ return `${header_b64}.${payload_b64}.${sig_placeholder}`;
74
+ }
75
+
25
76
  let counter = Math.floor(Math.random() * 1_000_000) + 1_000_000;
26
77
 
27
78
  export function getTokensPlaceholders(params: { configId: string; tokens: Tokens }): Tokens {
@@ -48,66 +99,68 @@ export function getTokensPlaceholders(params: { configId: string; tokens: Tokens
48
99
  const id = counter++;
49
100
 
50
101
  const entry_new: (typeof entries)[number] = {
51
- id,
52
102
  configId,
103
+ id,
53
104
  tokens: {
54
- accessToken: tokens.accessToken,
55
105
  idToken: tokens.idToken,
106
+ accessToken: tokens.accessToken,
56
107
  refreshToken: tokens.refreshToken
108
+ },
109
+ tokens_placeholder: {
110
+ idToken: generatePlaceholderForToken({
111
+ tokenType: "id_token",
112
+ id,
113
+ token_real: tokens.idToken
114
+ }),
115
+ accessToken: generatePlaceholderForToken({
116
+ tokenType: "access_token",
117
+ id,
118
+ token_real: tokens.accessToken
119
+ }),
120
+ refreshToken:
121
+ tokens.refreshToken === undefined
122
+ ? undefined
123
+ : generatePlaceholderForToken({
124
+ tokenType: "refresh_token",
125
+ id,
126
+ token_real: tokens.refreshToken
127
+ })
57
128
  }
58
129
  };
59
130
 
60
131
  entries.push(entry_new);
61
132
 
62
- return {
63
- accessToken: `access_token_placeholder_${id}`,
64
- idToken: `id_token_placeholder_${id}`,
65
- refreshToken: tokens.refreshToken === undefined ? undefined : `refresh_token_placeholder_${id}`
66
- };
133
+ return entry_new.tokens_placeholder;
67
134
  }
68
135
 
69
- export function substitutePlaceholderByRealToken(params: {
70
- text: string;
71
- doEncodeUriComponent: boolean;
72
- }): string {
73
- const { text, doEncodeUriComponent } = params;
136
+ export function substitutePlaceholderByRealToken(text: string): string {
137
+ if (!text.includes("_placeholder_")) {
138
+ return text;
139
+ }
74
140
 
75
141
  let text_modified = text;
76
142
 
77
- for (const [tokenType, regExp] of [
78
- ["access_token", /access_token_placeholder_(\d+)/g],
79
- ["id_token", /id_token_placeholder_(\d+)/g],
80
- ["refresh_token", /refresh_token_placeholder_(\d+)/g]
81
- ] as const) {
82
- text_modified = text_modified.replace(regExp, (...[, p1]) => {
83
- const id = parseInt(p1);
84
-
85
- const entry = entries.find(e => e.id === id);
86
-
87
- if (!entry) {
88
- throw new Error(
89
- [
90
- "oidc-spa: Outdated token used to make a request.",
91
- "Token should not be stored at the application level, when a token",
92
- "is needed, it should be requested and used immediately."
93
- ].join(" ")
94
- );
95
- }
143
+ for (const entry of entries) {
144
+ if (!text.includes(`${entry.id}`)) {
145
+ continue;
146
+ }
147
+
148
+ for (const tokenType of ["idToken", "accessToken", "refreshToken"] as const) {
149
+ const placeholder = entry.tokens_placeholder[tokenType];
96
150
 
97
- const token = (() => {
98
- switch (tokenType) {
99
- case "access_token":
100
- return entry.tokens.accessToken;
101
- case "id_token":
102
- return entry.tokens.idToken;
103
- case "refresh_token":
104
- assert(entry.tokens.refreshToken !== undefined, "204392284");
105
- return entry.tokens.refreshToken;
151
+ if (tokenType === "refreshToken") {
152
+ if (placeholder === undefined) {
153
+ continue;
106
154
  }
107
- })();
155
+ }
156
+ assert(placeholder !== undefined, "023948092393");
157
+
158
+ const realToken = entry.tokens[tokenType];
108
159
 
109
- return doEncodeUriComponent ? encodeURIComponent(token) : token;
110
- });
160
+ assert(realToken !== undefined, "02394809239328");
161
+
162
+ text_modified = text_modified.split(placeholder).join(realToken);
163
+ }
111
164
  }
112
165
 
113
166
  return text_modified;
@@ -1,4 +1,11 @@
1
- import { useState, useEffect, useReducer, type ReactNode, type ComponentType } from "react";
1
+ import {
2
+ useState,
3
+ useEffect,
4
+ useReducer,
5
+ createElement,
6
+ type ReactNode,
7
+ type ComponentType
8
+ } from "react";
2
9
  import type { UseOidc, OidcSpaApi, GetOidc, ParamsOfBootstrap } from "./types";
3
10
  import type { ZodSchemaLike } from "../tools/ZodSchemaLike";
4
11
  import type { Oidc as Oidc_core } from "../core";
@@ -521,7 +528,7 @@ export function createOidcSpaApi<
521
528
  if (oidcCoreOrOidcInitializationError instanceof OidcInitializationError) {
522
529
  const oidcInitializationError = oidcCoreOrOidcInitializationError;
523
530
 
524
- return <ErrorComponent oidcInitializationError={oidcInitializationError} />;
531
+ return createElement(ErrorComponent, { oidcInitializationError });
525
532
  }
526
533
 
527
534
  return children;
@@ -574,7 +581,7 @@ export function createOidcSpaApi<
574
581
  throw oidcCore.login({ doesCurrentHrefRequiresAuth: true });
575
582
  }
576
583
 
577
- return <Component {...props} />;
584
+ return createElement(Component, props);
578
585
  }
579
586
 
580
587
  ComponentWithLoginEnforced.displayName = `${
@@ -1,7 +1,7 @@
1
1
  import type { OidcSpaVitePluginParams } from "./vite-plugin";
2
2
  import type { ResolvedConfig } from "vite";
3
3
  import type { PluginContext } from "rollup";
4
- import { promises as fs } from "node:fs";
4
+ import { promises as fs, readFileSync, existsSync } from "node:fs";
5
5
  import * as path from "node:path";
6
6
  import { assert } from "../tools/tsafe/assert";
7
7
  import type { Equals } from "../tools/tsafe/Equals";
@@ -22,8 +22,6 @@ type EntryResolution = {
22
22
 
23
23
  const ORIGINAL_QUERY_PARAM = "oidc-spa-original";
24
24
 
25
- const GENERIC_ENTRY_CANDIDATES = ["src/main.tsx", "src/main.ts", "src/main.jsx", "src/main.js"];
26
-
27
25
  const REACT_ROUTER_ENTRY_CANDIDATES = [
28
26
  "entry.client.tsx",
29
27
  "entry.client.ts",
@@ -63,7 +61,8 @@ export function createHandleClientEntrypoint(params: {
63
61
  const isOriginalRequest = queryParams.getAll(ORIGINAL_QUERY_PARAM).includes("true");
64
62
 
65
63
  if (isOriginalRequest) {
66
- return loadOriginalModule(entryResolution, pluginContext);
64
+ entryResolution.watchFiles.forEach(file => pluginContext.addWatchFile(file));
65
+ return fs.readFile(entryResolution.absolutePath, "utf8");
67
66
  }
68
67
 
69
68
  entryResolution.watchFiles.forEach(file => pluginContext.addWatchFile(file));
@@ -247,34 +246,152 @@ function resolveEntryForProject({
247
246
  }
248
247
 
249
248
  case "other": {
250
- const candidate = resolveCandidate({
251
- root,
252
- subDirectories: ["."],
253
- filenames: GENERIC_ENTRY_CANDIDATES
254
- });
249
+ const indexHtmlPath = (() => {
250
+ const rollupInput = config.build.rollupOptions?.input;
251
+
252
+ const htmlCandidates: string[] = [];
253
+
254
+ const addCandidate = (maybePath: string) => {
255
+ const candidate = path.isAbsolute(maybePath)
256
+ ? maybePath
257
+ : path.resolve(root, maybePath);
258
+
259
+ if (path.extname(candidate).toLowerCase() === ".html") {
260
+ htmlCandidates.push(candidate);
261
+ }
262
+ };
263
+
264
+ if (typeof rollupInput === "string") {
265
+ addCandidate(rollupInput);
266
+ } else if (Array.isArray(rollupInput)) {
267
+ rollupInput.forEach(addCandidate);
268
+ } else if (rollupInput && typeof rollupInput === "object") {
269
+ Object.values(rollupInput).forEach(addCandidate);
270
+ }
271
+
272
+ if (htmlCandidates.length > 1) {
273
+ throw new Error(
274
+ [
275
+ "oidc-spa: Multiple HTML inputs detected in Vite configuration.",
276
+ `Found: ${htmlCandidates.join(", ")}.`,
277
+ "No worries, if the oidc-spa Vite plugin fails you can still configure the client entrypoint manually.",
278
+ "Please refer to the documentation for more details."
279
+ ].join(" ")
280
+ );
281
+ }
255
282
 
256
- assert(candidate !== undefined);
283
+ const defaultIndexHtml = path.resolve(root, "index.html");
257
284
 
258
- const normalized = normalizeAbsolute(candidate);
285
+ const indexHtmlPath =
286
+ htmlCandidates[0] ?? (existsSync(defaultIndexHtml) ? defaultIndexHtml : undefined);
259
287
 
260
- const resolution: EntryResolution = {
261
- absolutePath: candidate,
262
- normalizedPath: normalized,
263
- watchFiles: [candidate]
264
- };
288
+ if (indexHtmlPath === undefined) {
289
+ throw new Error(
290
+ [
291
+ "oidc-spa: Could not locate index.html.",
292
+ "Checked Vite rollupOptions.input for HTML entries and the default index.html at the project root.",
293
+ "No worries, if the oidc-spa Vite plugin fails you can still configure the client entrypoint manually.",
294
+ "Please refer to the documentation for more details."
295
+ ].join(" ")
296
+ );
297
+ }
265
298
 
266
- return resolution;
299
+ return indexHtmlPath;
300
+ })();
301
+
302
+ const indexHtmlContent = readFileSync(indexHtmlPath, "utf8");
303
+
304
+ const bodyMatch = indexHtmlContent.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
305
+ const bodyContent = bodyMatch?.[1] ?? indexHtmlContent;
306
+
307
+ const moduleScriptSrcs: string[] = [];
308
+ const scriptRegex =
309
+ /<script\b[^>]*\btype\s*=\s*["']module["'][^>]*\bsrc\s*=\s*["']([^"']+)["'][^>]*>/gi;
310
+
311
+ let match: RegExpExecArray | null;
312
+ // eslint-disable-next-line no-cond-assign
313
+ while ((match = scriptRegex.exec(bodyContent)) !== null) {
314
+ const [, src] = match;
315
+ moduleScriptSrcs.push(src);
316
+ }
317
+
318
+ if (moduleScriptSrcs.length === 0) {
319
+ throw new Error(
320
+ [
321
+ 'oidc-spa: Could not find a <script type="module" src="..."> tag in index.html.',
322
+ "No worries, if the oidc-spa Vite plugin fails you can still configure the client entrypoint manually.",
323
+ "Please refer to the documentation for more details."
324
+ ].join(" ")
325
+ );
326
+ }
327
+
328
+ if (moduleScriptSrcs.length > 1) {
329
+ throw new Error(
330
+ [
331
+ "oidc-spa: Unable to determine a unique client entrypoint from index.html.",
332
+ `Found multiple <script type=\"module\" src=\"...\"> tags: ${moduleScriptSrcs.join(
333
+ ", "
334
+ )}.`,
335
+ "No worries, if the oidc-spa Vite plugin fails you can still configure the client entrypoint manually.",
336
+ "Please refer to the documentation for more details."
337
+ ].join(" ")
338
+ );
339
+ }
340
+
341
+ const [rawSrc] = moduleScriptSrcs;
342
+ const cleanedSrc = rawSrc.replace(/[?#].*$/, "");
343
+
344
+ if (/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(cleanedSrc)) {
345
+ throw new Error(
346
+ [
347
+ "oidc-spa: The client entrypoint in index.html points to an external URL,",
348
+ `got "${rawSrc}".`,
349
+ "\nNo worries, if the oidc-spa Vite plugin fails you can still configure the client entrypoint manually.",
350
+ "Please refer to the documentation for more details."
351
+ ].join(" ")
352
+ );
353
+ }
354
+
355
+ const indexDir = path.dirname(indexHtmlPath);
356
+
357
+ const absoluteCandidates = (() => {
358
+ const resolvedPath = cleanedSrc.startsWith("/")
359
+ ? path.join(root, cleanedSrc.replace(/^\//, ""))
360
+ : path.resolve(indexDir, cleanedSrc);
361
+
362
+ const hasExtension = path.extname(resolvedPath) !== "";
363
+
364
+ if (hasExtension) {
365
+ return [resolvedPath];
366
+ }
367
+
368
+ const extensions = [".tsx", ".ts", ".jsx", ".js"];
369
+
370
+ return extensions.map(ext => `${resolvedPath}${ext}`);
371
+ })();
372
+
373
+ const existingCandidate = absoluteCandidates.find(candidate => existsSync(candidate));
374
+
375
+ if (!existingCandidate) {
376
+ throw new Error(
377
+ [
378
+ "oidc-spa: Could not locate the client entrypoint referenced in index.html.",
379
+ `Found src="${rawSrc}" and tried: ${absoluteCandidates.join(", ")}.`,
380
+ "Please ensure the file exists or configure the client entrypoint manually.",
381
+ "\nNo worries, if the oidc-spa Vite plugin fails you can still configure the client entrypoint manually.",
382
+ "Please refer to the documentation for more details."
383
+ ].join(" ")
384
+ );
385
+ }
386
+
387
+ return {
388
+ absolutePath: existingCandidate,
389
+ normalizedPath: normalizeAbsolute(existingCandidate),
390
+ watchFiles: [indexHtmlPath, existingCandidate]
391
+ };
267
392
  }
268
393
 
269
394
  default:
270
395
  assert<Equals<typeof projectType, never>>(false);
271
396
  }
272
397
  }
273
-
274
- function loadOriginalModule(
275
- entry: EntryResolution,
276
- context: { addWatchFile(id: string): void }
277
- ): Promise<string> {
278
- entry.watchFiles.forEach(file => context.addWatchFile(file));
279
- return fs.readFile(entry.absolutePath, "utf8");
280
- }