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.
- package/core/createOidc.js +16 -1
- package/core/createOidc.js.map +1 -1
- package/core/tokenExfiltrationDefense.js +17 -44
- package/core/tokenExfiltrationDefense.js.map +1 -1
- package/core/tokenPlaceholderSubstitution.d.ts +2 -5
- package/core/tokenPlaceholderSubstitution.js +73 -38
- package/core/tokenPlaceholderSubstitution.js.map +1 -1
- package/esm/core/createOidc.js +16 -1
- package/esm/core/createOidc.js.map +1 -1
- package/esm/core/tokenExfiltrationDefense.js +18 -45
- package/esm/core/tokenExfiltrationDefense.js.map +1 -1
- package/esm/core/tokenPlaceholderSubstitution.d.ts +2 -5
- package/esm/core/tokenPlaceholderSubstitution.js +72 -37
- package/esm/core/tokenPlaceholderSubstitution.js.map +1 -1
- package/esm/react-spa/createOidcSpaApi.js +3 -4
- package/esm/react-spa/createOidcSpaApi.js.map +1 -1
- package/esm/tanstack-start/react/createOidcSpaApi.js.map +1 -1
- package/esm/tanstack-start/react/disableSsrIfLoginEnforced.js.map +1 -1
- package/package.json +1 -1
- package/react-spa/createOidcSpaApi.js +2 -3
- package/react-spa/createOidcSpaApi.js.map +1 -1
- package/src/core/createOidc.ts +18 -0
- package/src/core/tokenExfiltrationDefense.ts +18 -45
- package/src/core/tokenPlaceholderSubstitution.ts +98 -45
- package/src/react-spa/{createOidcSpaApi.tsx → createOidcSpaApi.ts} +10 -3
- package/src/vite-plugin/handleClientEntrypoint.ts +142 -25
- package/vite-plugin/handleClientEntrypoint.js +104 -18
- package/vite-plugin/handleClientEntrypoint.js.map +1 -1
- /package/src/tanstack-start/react/{createOidcSpaApi.tsx → createOidcSpaApi.ts} +0 -0
- /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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
70
|
-
text
|
|
71
|
-
|
|
72
|
-
}
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
283
|
+
const defaultIndexHtml = path.resolve(root, "index.html");
|
|
257
284
|
|
|
258
|
-
|
|
285
|
+
const indexHtmlPath =
|
|
286
|
+
htmlCandidates[0] ?? (existsSync(defaultIndexHtml) ? defaultIndexHtml : undefined);
|
|
259
287
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
}
|